卢克 (luqui) 提出了可能是思考问题的最优雅的方式。但是,他的编码要求您为要创建的每个此类重写规则手动获取正确的大片遍历。
Bjorn Bringert 来自A Pattern for Nearly Composable Functions 的组合可以使这更容易,特别是如果您需要编写多个这样的规范化通道。它通常用 Applicatives 或 rank 2 类型编写,但为了简单起见,我将推迟:
给定您的数据类型
data LogicalExpression
= Var Char
| Neg LogicalExpression
| Conj LogicalExpression LogicalExpression
| Disj LogicalExpression LogicalExpression
| Impl LogicalExpression LogicalExpression
deriving (Show)
我们可以定义一个用于寻找非顶级子表达式的类:
class Compos a where
compos' :: (a -> a) -> a -> a
instance Compos LogicalExpression where
compos' f (Neg e) = Neg (f e)
compos' f (Conj a b) = Conj (f a) (f b)
compos' f (Disj a b) = Disj (f a) (f b)
compos' f (Impl a b) = Impl (f a) (f b)
compos' _ t = t
例如,我们可以消除所有影响:
elimImpl :: LogicalExpression -> LogicalExpression
elimImpl (Impl a b) = Disj (Not (elimImpl a)) (elimImpl b)
elimImpl t = compos' elimImpl t -- search deeper
然后我们可以应用它,就像上面的 luqui 一样,寻找否定的连词和析取词。而且,正如 Luke 指出的那样,一次完成所有否定分布可能会更好,因此我们还将包括否定蕴涵和双重否定消除的归一化,产生否定范式的公式(假设我们已经消除暗示)
nnf :: LogicalExpression -> LogicalExpression
nnf (Neg (Conj a b)) = Disj (nnf (Neg a)) (nnf (Neg b))
nnf (Neg (Disj a b)) = Conj (nnf (Neg a)) (nnf (Neg b))
nnf (Neg (Neg a)) = nnf a
nnf t = compos' nnf t -- search and replace
关键是最后一行,它表示如果上述其他情况都不匹配,则去寻找可以应用此规则的子表达式。此外,由于我们将 推Neg
入术语,然后对它们进行规范化,因此您应该只在叶子处使用否定变量,因为Neg
在另一个构造函数之前的所有其他情况都将被规范化。
更高级的版本将使用
import Control.Applicative
import Control.Monad.Identity
class Compos a where
compos :: Applicative f => (a -> f a) -> a -> f a
compos' :: Compos a => (a -> a) -> a -> a
compos' f = runIdentity . compos (Identity . f)
和
instance Compos LogicalExpression where
compos f (Neg e) = Neg <$> f e
compos f (Conj a b) = Conj <$> f a <*> f b
compos f (Disj a b) = Disj <$> f a <*> f b
compos f (Impl a b) = Impl <$> f a <*> f b
compos _ t = pure t
这对您的特定情况没有帮助,但如果您需要返回多个重写结果、执行IO
或以其他方式在重写规则中参与更复杂的活动,这将很有用。
您可能需要使用它,例如,如果您想尝试将德摩根定律应用于它们适用的位置的任何子集,而不是追求正常形式。
请注意,无论您要重写什么函数,使用什么 Applicative,甚至在遍历过程中信息流的方向性,compos
每个数据类型只需要给出一次定义。