这看起来是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"))
这是您的原始代码,其中包含冗余(额外的Fixes 使情况变得更糟)。
-- 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 中,它构建了一个catamorphism,即一个函数
- 将公式分解为子组件
- 递归地将
cata f 应用于找到的子组件
- 将转换后的组件重新组合成公式
- 将最后一个公式(带有已处理的子公式)传递给
f,这样最上面的连接词就会受到影响
最后一步是做实际工作的那一步。上面的\case 只执行所需的转换。其他一切都由cata 处理(以及自动生成的Functor 实例)。
综上所述,我不建议任何人轻易搬到
recursion-schemes。使用cata 可以产生非常优雅的代码,但它需要理解所涉及的机制,这可能不是立即掌握(这肯定不适合我)。