Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Keyword arguments with do-block

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?

like image 786
Silvio Mayolo Avatar asked Nov 28 '22 22:11

Silvio Mayolo


1 Answers

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.

like image 185
Aleksei Matiushkin Avatar answered Dec 19 '22 07:12

Aleksei Matiushkin