How to design function that take both another funtion and a keyword as parameters

If a function takes a keyword as one of its parameters, we normally put it last:

def my_func(a, b, attrs \\ []) do
...
end

So at call site we can write: my_fun(1, 2, option: true)

Similarly, if a function takes another function as one of its parameters, we normally put it last:

def my_fun(a, b, func) do
    func.(a, b)
end

So at the call site, we can write nicely as:

my_fun(1, 2, fn a, b ->
    a + b
end)

Now, what if my function take both a keyword and a function as parameters?

function last

def my_fun(arg, keyword, func) do
...
end

...
my_fun(1, [
    option1: true,
    option2: false
], fn arg ->
    arg + 1
end)

Or:

keyword last

def my_fun(arg, func, keyword \\ []) do
...
end

...
my_fun(1,  fn arg ->
    arg + 1
end,
    option1: true,
    option2: false
)

The problem of function last is that keyword cannot be optional and I have to write [] around it. The problem of keyword last is that function is usually longer than the keyword, so it feels top heavy.

I played around a bit, and it seems like I can do:

high order function

defp private_fun(arg, func, keyword) do
...
end

def my_fun(arg, keyword \\ []) do
    &private_fun(arg, &1, keyword)
end

So, at the call site I can do:

my_fun(1,
    option1: true,
    option2: false
).(fn arg ->
    arg + 1
end)

My question is, does it look weird? Because I’ve not seen other people do this.

2 Likes

Well Task.async_stream has no problem with having a function parameter and then keyword list as a last parameter, by the way.

I personally would just put the function inside the keyword list, which gives us the nice advantage of explaining what is the goal of the function.

And then you can do stuff like:

do_stuff(
  username,
  password,
  token_expiration_policy: :hourly,
  callback: fn token -> refresh_token(token) end
)
1 Like

So you prefer to put the function as the last in the keyword list. It looks nice on the call site, not so on the definition site. In my case the function is mandatory, so the keyword list become mandatory too. Also, I’d have to write the code to handle the case when the caller forgot to add the callback function.

1 Like

I agree it’s tedious to code. I used NimbleOptions with some success but was left unsatisfied. The best dev UX that I have achieved for myself was basically a macro to raise an error if a minimum set of keys weren’t present – right at the start of the function – and then operate more or less as usual after.

But even that still doesn’t rid you of having to validate the values passed in the keyword list.

In short, I don’t like keyword lists very much for the exact reasons you listed when saying that my idea makes for suckier coding of the definition.

1 Like

I agree it does look a bit awkward, but particularly when the keyword is a list of options it seems the most semantically correct. As @dimitarvp pointed out there is precedent in the standard library and in this case that’s enough for me. I would certainly prefer it to the version you’re experimenting with :sweat_smile:

1 Like

ExUnit.CaptureIO was just being discussed in the past day or so and it does keywords first and function last—see the last example. So there is precedent in core!

capture_io([input: "this is input", capture_prompt: false], fn ->
  # ...
end

It’s optional, too! You just need to use guards:

def my_fun(fun) when is_function(fun), do: # ...
def my_fun(options, fun), do: # ...

EDIT: I realize in that specific example you don’t need to use guards since my_fun/1 and my_fun/2 are different functions, but with more args it may be necessary.

1 Like

Isn’t this a moderately close cousin of everyday do-end blocks? Those de-sugar to a do: func keyword list as it is, as do if/else. It’s the most clearly visible if you call quote on a one-liner such as if bool, do: ok to see the AST.

I wonder if you could use this technique directly yourself.

It is! But it would need to be a macro which may not be desirable.

Thanks. I started with a keyword last implementation, then I feel the awkwardness in many call sites. I will use your suggestion of using function clause guard to make the last 2 parameters swap-able so users can pick either keyword last or function last.

1 Like

if..., do: feels natual because one can draw parallel from other programing language. If I make some crazy macro, i fear it would be even harder to read than high-order function. :frowning:

I was actually gonna say that @shanesveller’s idea may actually be the cleanest but I don’t know really know your full use-case! If you need to be dynamically passing functions around then a fun is certainly better I’d say.

On the flipside, there’s String.replace/4 which is fun then kwlist:

@spec replace(t(), pattern() | Regex.t(), t() | (t() -> t() | iodata()), keyword()) :: t()

:man_shrugging:

1 Like

Well I’ll be :face_with_hand_over_mouth: