Это выглядит как прекрасный случай для recursion-schemes.
Во-первых, мы описываем ваш тип Sentence sym
как фиксированную точку уровня подходящего функтора.
{-# LANGUAGE DeriveFunctor, LambdaCase #-}
import Data.Functor.Foldable -- from the recursion-schemes package
-- The functor describing the recursive data type
data SentenceF sym r
= AtomicSentence sym
| ImplySentence r r
| AndSentence r r
| OrSentence r r
| NotSentence r
deriving (Functor, Show)
-- The original type recovered via a fixed point
type Sentence sym = Fix (SentenceF sym)
выше Sentence sym
типа почти идентичен исходному, за исключением того что все должно быть обернуто внутри Fix
. Адаптация оригинального кода для использования этого типа полностью механическая: где мы использовали (Constructor ...)
, теперь мы используем Fix (Constructor ...)
. Например
type Symbol = String
-- A simple formula: not (p -> (p || q))
testSentence :: Sentence Symbol
testSentence =
Fix $ NotSentence $
Fix $ ImplySentence
(Fix $ AtomicSentence "p")
(Fix $ OrSentence
(Fix $ AtomicSentence "p")
(Fix $ AtomicSentence "q"))
Вот исходный код, с его увольнениями (усугубляются дополнительных Fix
х годов).
-- The original code, adapted
imply_remove :: Sentence Symbol -> Sentence Symbol
imply_remove (Fix (ImplySentence s1 s2)) =
Fix $ OrSentence (Fix $ NotSentence (imply_remove s1)) (imply_remove s2)
imply_remove (Fix (AndSentence s1 s2)) =
Fix $ AndSentence (imply_remove s1) (imply_remove s2)
imply_remove (Fix (OrSentence s1 s2)) =
Fix $ OrSentence (imply_remove s1) (imply_remove s2)
imply_remove (Fix (NotSentence s1)) =
Fix $ NotSentence (imply_remove s1)
imply_remove (Fix (AtomicSentence s1)) =
Fix $ AtomicSentence s1
Давайте выполнить тест по оценке imply_remove testSentence
: результат чего мы ожидаем:
-- Output: not ((not p) || (p || q))
Fix (NotSentence
(Fix (OrSentence
(Fix (NotSentence (Fix (AtomicSentence "p"))))
(Fix (OrSentence
(Fix (AtomicSentence "p"))
(Fix (AtomicSentence "q")))))))
А теперь, давайте использовать ядерное оружие, заимствованные из рекурсии-схем:
imply_remove2 :: Sentence Symbol -> Sentence Symbol
imply_remove2 = cata $ \case
-- Rewrite ImplySentence as follows
ImplySentence s1 s2 -> Fix $ OrSentence (Fix $ NotSentence s1) s2
-- Keep everything else as it is (after it had been recursively processed)
s -> Fix s
Если мы проведем тест imply_remove2 testSentence
, мы получим тот же результат, что и исходный код.
Что делает cata
? Очень грубо, когда применяется к такой функции, как в cata f
, он создает катамарфит , т.е.функция, которая
- принимает формулу друг от друга в его подкомпонентов
- рекурсивно применить
cata f
к найденным подкомпонентов
- собирает преобразованные компоненты в формулу
- проходит эта последняя формула (с обработанной подформулах) до
f
так что может быть затронуто самое верхнее соединение.
Последний шаг - тот, который выполняет фактическую работу. \case
выше выполняет только требуемое преобразование. Все остальное обрабатывается cata
(и экземпляр Functor
, который был автоматически сгенерирован).
Все сказанное выше, я бы не рекомендовал кому-либо слегка перейти на recursion-schemes
. Использование cata
может привести к очень изящному коду, но для этого требуется понять, какое задействованное оборудование, которое не может быть сразу понято (это наверняка не для меня).
Я бы предложил библиотеку [синтаксис] (https://hackage.haskell.org/package/syntactic). – crockeea