В настоящее время я работаю над простым интерпретатором языка программирования, и у меня есть такой тип данных:
data Expr
= Variable String
| Number Int
| Add [Expr]
| Sub Expr Expr
И у меня есть много функций, которые делают простые вещи, такие как:
-- Substitute a value for a variable
substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = go
where
go (Variable x)
| x == name = Number newValue
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
-- Replace subtraction with a constant with addition by a negative number
replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = go
where
go (Sub x (Number y)) =
Add [go x, Number (-y)]
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
Но в каждой из этих функций я должен повторить ту часть, которая вызывает код рекурсивно, с небольшим изменением одной части функции. Есть ли какой-нибудь способ сделать это более обобщенно? Я бы предпочел не копировать и вставлять эту часть:
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
И просто меняйте один случай каждый раз, потому что кажется неэффективным дублировать код, подобный этому.
Единственное решение, которое я мог бы придумать, - это иметь функцию, которая вызывает функцию сначала для всей структуры данных, а затем рекурсивно для результата, подобного этому:
recurseAfter :: (Expr -> Expr) -> Expr -> Expr
recurseAfter f x =
case f x of
Add xs ->
Add $ map (recurseAfter f) xs
Sub x y ->
Sub (recurseAfter f x) (recurseAfter f y)
other -> other
substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue =
recurseAfter $ \case
Variable x
| x == name -> Number newValue
other -> other
replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd =
recurseAfter $ \case
Sub x (Number y) ->
Add [x, Number (-y)]
other -> other
Но я чувствую, что, вероятно, должен быть более простой способ сделать это уже. Я что-то пропустил?
Add :: Expr -> Expr -> Expr
вместоAdd :: [Expr] -> Expr
, и избавиться отSub
всего.recurseAfter
ISana
в маскировке. Возможно, вы захотите посмотреть на анаморфизмы иrecursion-schemes
. При этом, я думаю, ваше окончательное решение настолько короткое, насколько это возможно. Переход на официальныеrecursion-schemes
анаморфизмы не спасет много.Ответы:
Поздравляю, вы только что заново обнаружили анаморфизмы!
Вот ваш код, перефразированный так, чтобы он работал с
recursion-schemes
пакетом. Увы, он не короче, потому что нам нужен шаблон для работы оборудования. (Там может быть какой-то автоматический способ избежать шаблон, например, с помощью дженериков. Я просто не знаю.)Ниже ваш
recurseAfter
заменен на стандартныйana
.Сначала мы определим ваш рекурсивный тип, а также функтор, для которого он является фиксированной точкой.
Затем мы связываем эти два с несколькими экземплярами, чтобы мы могли развернуться
Expr
в изоморфныйExprF Expr
и сложить его обратно.Наконец, мы адаптируем ваш оригинальный код и добавим пару тестов.
Альтернативой может быть
ExprF a
только определение , а затем выводtype Expr = Fix ExprF
. Это экономит некоторые из вышеприведенных шаблонов (например, два экземпляра) за счет необходимости использоватьFix (VariableF ...)
вместоVariable ...
, а также аналогично для других конструкторов.Можно также смягчить это, используя синонимы паттернов (хотя и ценой чуть большего количества шаблонов).
Обновление: я наконец-то нашел инструмент для автоматизации, использующий шаблон Haskell. Это делает весь код достаточно коротким. Обратите внимание, что
ExprF
функтор и два вышеупомянутых экземпляра все еще существуют под капотом, и мы все еще должны их использовать. Мы избавляем вас от необходимости определять их вручную, но это экономит много усилий.источник
Expr
явно, а не что-то вродеtype Expr = Fix ExprF
?Fix
+ настоящий конструктор. Использование последнего подхода с автоматизацией TH приятнее, IMO.В качестве альтернативного подхода это также типичный вариант использования
uniplate
пакета. Он может использоватьData.Data
дженерики, а не Template Haskell, чтобы сгенерировать шаблон, так что если вы получаетеData
экземпляры для вашегоExpr
:тогда
transform
функция изData.Generics.Uniplate.Data
применяет функцию рекурсивно к каждому вложенномуExpr
:Обратите внимание, что, в
replaceSubWithAdd
частности, функцияf
написана для выполнения нерекурсивной замены;transform
делает его рекурсивнымx :: Expr
, поэтому он выполняет ту же магию для вспомогательной функции, чтоana
и в ответе @ chi:Это не короче, чем решение Template Haskell от @ chi. Одним из потенциальных преимуществ является то, что он
uniplate
предоставляет некоторые дополнительные функции, которые могут быть полезны. Например, если вы используетеdescend
вместоtransform
него, он преобразует только непосредственных потомков, которые могут дать вам контроль над тем, где происходит рекурсия, или вы можете использовать,rewrite
чтобы повторно преобразовать результат преобразований, пока не достигнете фиксированной точки. Одним потенциальным недостатком является то, что «анаморфизм» звучит намного круче, чем «одноплатный».Полная программа:
источник