скажем, у нас есть 2 структуры:
defmodule Algo.A do
defstruct id: nil, foo: nil
end
defmodule Algo.B do
defstruct id: nil, bar: nil
end
И следующий API, который фильтрует по типу структуры:
defmodule Algo do
alias Algo.{A, B}
def filter_by_type(collection, %A{}) do
for %A{} = el <- collection, do: el
end
def filter_by_type(collection, %B{}) do
for %B{} = el <- collection, do: el
end
end
Предполагаемое поведение состоит в том, чтобы просто вернуть коллекцию на основе совпадения в понимании:
test "filters by struct type" do
collection = [%A{id: 1}, %A{id: 2}, %B{id: 3}]
assert [%A{id: 1}, %A{id: 2}] = Algo.filter_by_type(collection, %A{})
assert [%B{id: 3}] = Algo.filter_by_type(collection, %B{})
end
Я хотел бы заменить его макросом примерно так:
for type <- [%A{}, %B{}] do
type = Macro.escape(type)
def filter_by_type(collection, unquote(type)) do
for unquote(type) = el <- collection, do: el
end
end
Это не работает, потому что значение под unquote
при понимании оценивается как [__struct__: Algo.A, foo: nil, id: nil]
, которое соответствует foo = nil
и id: nil
.
Как точно сопоставить только имя структуры, просто чтобы имитировать то, что находится в верхнем фрагменте?
Нет, у меня есть 2 частные функции, которые отличаются только типом структуры, используемой для сопоставления с образцом, как указано выше. Протоколов было бы слишком много, я думаю. Я хочу сделать его немного СУХИМ.
Проблема заключается в разнице между Macro.escape/2 и quote/2.
Macro.escape/2
как функция принимает термин и возвращает AST, представляющий этот термин.
quote/2
в качестве макроса принимает некоторый код и возвращает AST, представляющий этот код.
iex> Macro.escape %Algo.A{}
{:%{}, [], [__struct__: Algo.A, foo: nil, id: nil]}
iex> quote do %Algo.A{} end
{:%, [], [{:__aliases__, [alias: false], [:Algo, :A]}, {:%{}, [], []}]}
%struct{}
как код не совсем то же самое, что %struct{}
как термин, хотя это часто приводит к таковым. Код представляет собой вызов макроса %struct{}, а не настоящий литерал. Когда %struct{]
используется в совпадении, сопоставляются только предоставленные ключи. Использование версии Macro.escape/2
расширяет все пары ключей, прежде чем определить соответствие для функции.
Таким образом, самым простым изменением будет использование quote/2
в списке типов:
for type <- [quote do %A{} end, quote do %B{} end] do
def filter_by_type(collection, unquote(type)) do
# ...
Это больше текста для поддерживаемого типа, но вы можете сократить его, просто включив имена модулей. Это не было бы точно эквивалентно (вы бы использовали буквальные атомы вместо псевдонима AST), но, исходя из моего опыта, это будет работать так же:
for type <- [A, B] do
def filter_by_type(collection, %unquote(type){})
Но для этого даже не нужно метапрограммирование. Вы можете просто динамически сопоставлять структуру:
# include "where struct in [A, B]" if you want to restrict structs
def filter_by_type(collection, %struct{}) do
for %^struct{} = el <- collection, do: el
end
Или вы можете просто передать модуль вместо структуры, если она соответствует вашему варианту использования:
def filter_by_type(collection, struct) do
for %^struct{} = el <- collection, do: el
end
О, я не знал, что я могу закреплять такие структуры! Спасибо за подробный ответ!
Рассматривали ли вы использование протокола для этого? У вас может быть несколько его реализаций, но вызываемая реализация зависит от 1-го аргумента, поэтому вам нужно будет сделать
filter_by_type(type, collection)