关键字参数与do块

8
我有一个函数,大致如下。
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块以及其他关键字参数?


可以用宏代替函数吗? - bla
我对将其制作成宏没有任何异议。你有一个可以与宏一起使用的解决方案吗? - Silvio Mayolo
我会的。我会发布它,希望能对你有所帮助。 - bla
2个回答

20

虽然@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"

使用函数无法生成可执行代码块,因为函数是运行时对象。例如上面的示例中,它将是静态的,因为函数执行时的代码已经编译完毕,这意味着无法向该代码块传递代码。也就是说,在函数本身之前,在调用者上下文中执行代码块内容:

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代码块的工作方式。这就是宏的作用:宏是编译时公民。传递给宏的do:参数的block将作为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

太好了!如果其他条件都相等,我们应该始终优先选择函数而不是宏。 函数更安全,对于之后的其他开发人员(或者可能是六个月后的你)来说也更易于维护。 - Onorio Catenacci
@OnorioCatenacci 嗯,这取决于情况。虽然函数编写、调试、测试和维护更容易,但宏提供了在位置注入AST的显式功能,有时特别实用。请查看核心模块Stream中的这些宏。它们比原始代码更简洁,并为已经很繁琐的代码带来了非常宝贵的清晰度。不用说,用函数无法实现这一点。 - Aleksei Matiushkin
感谢您的解释,我一直在想函数参数中的“do: expression”语法到底是做什么的... - Julian Rubisch

7

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

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

这正是我正在寻找的解决方案。非常感谢你! - Silvio Mayolo
我很高兴能够帮助。 :) - bla

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接