Это выглядит как хороший случай для схем рекурсии.
Во-первых, мы описываем ваш 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
es).
-- 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
может привести к очень элегантному коду, но требует понимания задействованного механизма, что, возможно, не сразу понять (это, конечно, не для меня).
person
chi
schedule
22.03.2015