Аргументы ключевых слов с блокировкой

У меня есть функция, которая выглядит примерно так.

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 в дополнение к другим аргументам ключевого слова в одном вызове функции?


person Silvio Mayolo    schedule 20.07.2018    source источник
comment
Может быть макрос вместо функции?   -  person bla    schedule 21.07.2018
comment
У меня нет никаких сомнений в том, чтобы превратить это в макрос. У вас есть решение, которое работает с макросом?   -  person Silvio Mayolo    schedule 21.07.2018
comment
Я делаю. Я опубликую это, надеюсь, это поможет вам.   -  person bla    schedule 21.07.2018


Ответы (2)


Если вы готовы использовать макрос вместо функции, это может вам помочь:

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
person bla    schedule 20.07.2018
comment
Это именно то решение, которое я искал. Большое спасибо! - person Silvio Mayolo; 21.07.2018
comment
Я рад, что смог помочь. :) - person bla; 21.07.2018

Хотя ответ, предоставленный @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

Обычно блоки Эликсира работают не так, как вы ожидаете. Вот когда на сцену выходит макрос: макросы являются гражданами времени компиляции. 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.

person Aleksei Matiushkin    schedule 21.07.2018
comment
Отличный момент! Всегда следует предпочитать функцию макросу, если все остальное одинаково. Функции намного безопаснее и их намного проще поддерживать другим разработчикам после вас (или, может быть, вас через полгода). - person Onorio Catenacci; 23.07.2018
comment
@OnorioCatenacci хорошо, это зависит. В то время как функции проще писать, отлаживать, тестировать и поддерживать макросы предоставляют явную функцию внедрения AST на место, что иногда очень помогает. Проверьте от Stream основной модуль. Они более лаконичны, чем исходный код, и вносят очень ценную ясность в и без того громоздкий код. Излишне говорить, что этого нельзя добиться с помощью функций. - person Aleksei Matiushkin; 23.07.2018
comment
Спасибо за объяснение, мне было интересно, что именно делает синтаксис «do: expression» в аргументах функции... - person Julian Rubisch; 27.01.2019