Haskell递归数据类型的“默认行为”

时间:2015-03-22 22:15:11

标签: haskell functor recursive-datastructures

我试图在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'写

似乎仿函数与我想要的类似,但是没有成功...我想对子树进行操作,而不是对子树的每个节点中包含的某些值进行操作。

有没有正确的方法呢?或者更自然的方式来构建我的数据?

3 个答案:

答案 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 ,即一个

的函数
  1. 将公式分为子组件
  2. 以递归方式将cata f应用于找到的子组件
  3. 将转换后的组件重新组合成公式
  4. 将最后一个公式(已处理的子公式)传递给f,以便最顶层的连接可能会受到影响
  5. 最后一步是做实际工作的人。上面的\case只执行想要的转换。其他所有内容都由cata(以及自动生成的Functor实例)处理。

    以上所述,我不建议任何人轻易搬到 recursion-schemes。使用cata可以产生非常优雅的代码,但需要人们理解所涉及的机制,这可能无法立即掌握(当然不适合我)。

答案 1 :(得分:3)

您正在寻找的是通用编程'在哈斯克尔:https://wiki.haskell.org/Generics;一个早期的形式被称为" Scrap Your Boilerplate",你也可能想要谷歌。我还没有对此进行测试,但我认为如果您使用UniplateData.Generics.UniplateData.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

这种方法的优点是它可能更容易实现,因为它不需要任何依赖。