Макрос с сопоставлением с образцом в теле функции

скажем, у нас есть 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. Как точно сопоставить только имя структуры, просто чтобы имитировать то, что находится в верхнем фрагменте?

Рассматривали ли вы использование протокола для этого? У вас может быть несколько его реализаций, но вызываемая реализация зависит от 1-го аргумента, поэтому вам нужно будет сделать filter_by_type(type, collection)

Everett 07.02.2023 00:29

Нет, у меня есть 2 частные функции, которые отличаются только типом структуры, используемой для сопоставления с образцом, как указано выше. Протоколов было бы слишком много, я думаю. Я хочу сделать его немного СУХИМ.

user1453428 07.02.2023 07:39
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
52
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема заключается в разнице между 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

О, я не знал, что я могу закреплять такие структуры! Спасибо за подробный ответ!

user1453428 07.02.2023 07:27

Другие вопросы по теме