我试图在Haskell中编写一个命题逻辑解算器。我用递归数据类型表示逻辑表达式,称为句子'它有几种用于不同操作的子类型 - ' AndSentence' OrSentence'等等所以我猜它是一棵树,有几种类型的节点,每个节点都有0,1或2孩子。
它似乎有效,但有些代码有点重复,我认为应该有更好的表达方式。基本上我有几个功能,其中'默认行为'只是让函数以递归的方式对节点的子节点进行操作,从而在某些节点类型上触底(通常是' AtomicSentences',它们是叶子)。所以我写了一个函数:
imply_remove :: Sentence Symbol -> Sentence Symbol
imply_remove (ImplySentence s1 s2) = OrSentence (NotSentence (imply_remove s1)) (imply_remove s2)
imply_remove (AndSentence s1 s2) = AndSentence (imply_remove s1) (imply_remove s2)
imply_remove (OrSentence s1 s2) = OrSentence (imply_remove s1) (imply_remove s2)
imply_remove (NotSentence s1) = NotSentence (imply_remove s1)
imply_remove (AtomicSentence s1) = AtomicSentence s1
我希望用更简洁的方式为' AndSentence' OrSentence' OrSentence'以及' NotSentence'写
。似乎仿函数与我想要的类似,但是没有成功...我想对子树进行操作,而不是对子树的每个节点中包含的某些值进行操作。
有没有正确的方法呢?或者更自然的方式来构建我的数据?
答案 0 :(得分:5)
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
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
中,它构建了一个 catamorphism ,即一个
cata f
应用于找到的子组件f
,以便最顶层的连接可能会受到影响最后一步是做实际工作的人。上面的\case
只执行想要的转换。其他所有内容都由cata
(以及自动生成的Functor
实例)处理。
以上所述,我不建议任何人轻易搬到
recursion-schemes
。使用cata
可以产生非常优雅的代码,但需要人们理解所涉及的机制,这可能无法立即掌握(当然不适合我)。
答案 1 :(得分:3)
您正在寻找的是通用编程'在哈斯克尔:https://wiki.haskell.org/Generics;一个早期的形式被称为" Scrap Your Boilerplate",你也可能想要谷歌。我还没有对此进行测试,但我认为如果您使用Uniplate的Data.Generics.Uniplate
和Data.Generics.Uniplate.Data
模块,则可以将imply_remove
定义为
imply_remove = transform w where
w (ImplySentence s1 s2) = OrSentence (NotSentence s1) s2
w s = s
transform
为你做递归。
答案 2 :(得分:3)
您可以编写一个默认函数,用于定义如果不应用任何转换,应如何处理符号:
default_transformation :: (Sentence Symbol -> Sentence Symbol) -> Sentence Symbol -> Sentence Symbol
default_transformation f (ImplySentence s1 s2) = ImplySentence (f s1) (f s2)
default_transformation f (AndSentence s1 s2) = AndSentence (f s1) (f s2)
default_transformation f (OrSentence s1 s2) = OrSentence (f s1) (f s2)
default_transformation f (NotSentence s1) = NotSentence (f s1)
default_transformation f (AtomicSentence s1) = AtomicSentence s1
该函数将特定转换作为参数。
如果您编写特定转换,则只需编写与默认转换不同的情况,并将默认值添加为最后一种情况:
imply_remove :: Sentence Symbol -> Sentence Symbol
imply_remove (ImplySentence s1 s2) = OrSentence (NotSentence (imply_remove s1)) (imply_remove s2)
imply_remove s = default_transformation imply_remove s
这种方法的优点是它可能更容易实现,因为它不需要任何依赖。