I have a function that looks something like this.
def test(options \\ []) do
# Fun stuff happens here :)
end
It accepts several (optional) keyword arguments, including do:
. I'd like to be able to call it like this.
test foo: 1 do
"Hello"
end
However, this gives an error.
** (UndefinedFunctionError) function Example.test/2 is undefined or private. Did you mean one of: * test/0 * test/1 Example.test([foo: 1], [do: "Hello"]) (elixir) lib/code.ex:376: Code.require_file/2
As you can see from the error, the syntax above is desugaring to two separate keyword lists. Now, I can call this function using the following slightly inconvenient syntax
Example.test foo: 1, do: (
"Hello"
)
but is there any way to provide a do
-block in addition to other keyword arguments in one function call?
While the answer provided by @bla is technically correct (e. g. macro
works,) it barely sheds a light on whats and whys.
In the first place, nothing prevents you from having this syntax with a function instead of a macro, you just need to explicitly separate the keyword argument to do:
part and anything else:
defmodule Test do
# ⇓⇓⇓⇓⇓⇓⇓⇓⇓ HERE
def test(opts \\ [], do: block) do
IO.inspect(block)
end
end
Test.test foo: 1 do
"Hello"
end
#⇒ "Hello"
What you cannot achieve with a function, is to produce an executable block. It would be static, as in the example above, because functions are runtime citizens. The code at the moment of function execution will be already compiled, meaning one cannot pass a code to this block. That said, the block content will be executed withing the caller context, before the function itself:
defmodule Test do
def test(opts \\ [], do: block) do
IO.puts "In test"
end
end
Test.test foo: 1 do
IO.puts "In do block"
end
#⇒ In do block
# In test
This is not usually how do you expect Elixir blocks to work. That is when macro comes to the scene: macros are compile-time citizens. The block
passed to do:
argument of macro, will be injected as AST into Test.test/1
do
block, making
defmodule Test do
defmacro test(opts \\ [], do: block) do
quote do
IO.puts "In test"
unquote(block)
end
end
end
defmodule TestOfTest do
require Test
def test_of_test do
Test.test foo: 1 do
IO.puts "In do block"
end
end
end
TestOfTest.test_of_test
#⇒ In test
# In do block
Sidenote: in comments you stated “I have no qualms with making it into a macro.” This is plain wrong. Functions and macros are not interchangeable (although they look like they are,) they are completely different things. Macros should be used as a last resort. Macros inject AST inplace. Functions are AST.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With