带有 do-block 的关键字参数

Keyword arguments with do-block

我有一个看起来像这样的函数。

def test(options \ []) do
  # Fun stuff happens here :)
end

它接受几个(可选的)关键字参数,包括 do:。我希望能够这样称呼它。

test foo: 1 do
  "Hello"
end

然而,这给出了一个错误。

** (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

从错误中可以看出,上面的语法是将两个单独的关键字列表脱糖。现在,我可以使用以下稍微不方便的语法调用此函数

Example.test foo: 1, do: (
  "Hello"
)

但是除了一个函数调用中的其他关键字参数之外,还有什么方法可以提供 do 块吗?

如果您愿意使用宏而不是函数,这可能会对您有所帮助:

defmodule Example do
  defmacro test(args, do: block) do
    quote do
      IO.inspect(unquote(args))
      unquote(block)
    end
  end
end

示例用法:

iex(2)> defmodule Caller do
...(2)>   require Example
...(2)> 
...(2)>   def foo do
...(2)>     Example.test foo: 1 do
...(2)>       IO.puts "Inside block"
...(2)>     end
...(2)>   end
...(2)> end
{:module, Caller,
 <<70, 79, 82, 49, 0, 0, 4, 108, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 147,
   0, 0, 0, 16, 13, 69, 108, 105, 120, 105, 114, 46, 67, 97, 108, 108, 101, 114,
   8, 95, 95, 105, 110, 102, 111, 95, 95, ...>>, {:foo, 0}}
iex(3)> Caller.foo
[foo: 1]
Inside block
:ok

虽然@bla 提供的答案在技术上是正确的(例如 macro 有效),但它几乎没有说明是什么和为什么。

首先,没有什么能阻止您将此语法与函数而不是宏一起使用,您只需要明确地将关键字参数与 do: 部分和其他任何内容分开:

defmodule Test do
                     # ⇓⇓⇓⇓⇓⇓⇓⇓⇓ HERE 
  def test(opts \ [], do: block) do
    IO.inspect(block)
  end
end

Test.test foo: 1 do
  "Hello"
end
#⇒ "Hello"

不能用函数实现的是生成一个可执行文件块。它将是静态的,如上例所示,因为函数是运行时公民。函数执行时的代码将已经编译,这意味着不能将 代码 传递给此块。也就是说,块内容将在 caller 上下文中执行, 函数本身之前:

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

这通常不是您期望的 Elixir 块的工作方式。这就是宏出现的时候:宏是编译时公民。 block 传递给宏的 do: 参数,将被 作为 AST 注入到 Test.test/1 do 块中,使得

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

旁注: 你在评论中说“我毫不犹豫地把它变成一个宏。”这是完全错误的。函数和宏不可互换(尽管它们看起来是,)它们是完全不同的东西。宏应该作为最后的手段使用。宏 就地注入 AST。函数 是 AST.