Haskell中更简洁的模式匹配替代方案

25

目前,我有一些代码基本上像这样运作:

data Expression 
    = Literal Bool 
    | Variable String
    | Not Expression 
    | Or Expression Expression 
    | And Expression Expression
    deriving Eq

simplify :: Expression -> Expression
simplify (Literal b) = Literal b
simplify (Variable s) = Variable s
simplify (Not e) = case simplify e of
    (Literal b) -> Literal (not b)
    e'          -> Not e'
simplify (And a b) = case (simplify a, simplify b) of
    (Literal False, _) -> Literal False
    (_, Literal False) -> Literal False
    (a', b')           -> And a' b'
simplify (Or a b) = case (simplify a, simplify b) of
    (Literal True, _) -> Literal True
    (_, Literal True) -> Literal True
    (a', b')          -> Or a' b'

有很多关于简化布尔表达式的方式,包括许多对称的模式(例如And和Or模式)。然而,随着我添加更多运算符和规则,这些模式变得越来越多,而且感觉有点繁琐。特别是因为一些规则需要添加两次以考虑可交换性。

如何优美地重构大量模式,其中一些模式(我认为大多数模式)甚至是对称的?

目前,如果要将 And (Variable "x") (Not (Variable "x")) 简化为 Literal False,则需要添加两个嵌套规则,这显然不是最佳方案。


尝试将您的数据类型编写为GADT。这并不会使它更加清晰,但会使您拥有更多类型安全性。然后,您仍然可以应用下面任何一个解决方案。 - Jogger
我在这里使用GADTs会有什么好处? - Phil Kiener
是的,你说得对!我曾经认为像 Not (Variable "True") 这样的表达式不会通过类型检查,但重新思考后,Variable String 构造函数并没有返回特殊的东西。对于造成的困惑,我很抱歉! - Jogger
5个回答

13

问题基本上在于你必须一遍遍地写出每个子句中simplify的简化,这样做非常繁琐。最好先完成所有子表达式的简化,然后再考虑涉及顶层运算符的法则。一种简单的方法是添加一个辅助版本的simplify,它不会递归下去:

simplify :: Expression -> Expression
simplify (Literal b) = Literal b
simplify (Variable s) = Variable s
simplify (Not e) = simplify' . Not $ simplify e
simplify (And a b) = simplify' $ And (simplify a) (simplify b)
simplify (Or a b) = simplify' $ Or (simplify a) (simplify b)

simplify' :: Expression -> Expression
simplify' (Not (Literal b)) = Literal $ not b
simplify' (And (Literal False) _) = Literal False
...

在布尔运算中,你只有很少的操作可以使用,这可能是最明智的方法。然而,如果有更多的操作,避免在 simplify 中出现重复可能仍然是值得的。为此,你可以将一元和二元操作合并到一个公共构造函数中:

data Expression 
    = Literal Bool 
    | Variable String
    | BoolPrefix BoolPrefix Expression 
    | BoolInfix BoolInfix Expression Expression 
    deriving Eq

data BoolPrefix = Negation
data BoolInfix  = AndOp | OrOp

然后你就只有

simplify (Literal b) = Literal b
simplify (Variable s) = Variable s
simplify (BoolPrefix bpf e) = simplify' . BoolPrefix bpf $ simplify e
simplify (BoolInfix bifx a b) = simplify' $ BoolInfix bifx (simplify a) (simplify b)

显然,这使得simplify'变得更加棘手,因此可能不是一个好主意。但是,您可以通过定义专门的pattern synonyms来避免这种语法开销:

{-# LANGUAGE PatternSynonyms #-}

pattern Not :: Expression -> Expression
pattern Not x = BoolPrefix Negation x

infixr 3 :∧
pattern (:∧) :: Expression -> Expression -> Expression
pattern a:∧b = BoolInfix AndOp a b

infixr 2 :∨
pattern (:∨) :: Expression -> Expression -> Expression
pattern a:∨b = BoolInfix OrOp a b

就此而言,也许还有

pattern F, T :: Expression
pattern F = Literal False
pattern T = Literal True

有了那个,你就可以写了

simplify' :: Expression -> Expression
simplify' (Not (Literal b)) = Literal $ not b
simplify' (F :∧ _) = F
simplify' (_ :∧ F) = F
simplify' (T :∨ _) = T
simplify' (a :∧ Not b) | a==b  = T
...

我应该加上一个警告: 当我尝试类似于这些模式同义词的东西,不是针对布尔值而是仿射映射时,它使编译器变得极其缓慢。(此外,GHC-7.10还没有支持多态模式同义词; 现在情况已经发生了很大改变。)


注意,这并不总是能得到最简单的形式 - 要达到这个目的,您需要找到 simplify 的不动点。

模式同义词是一件很好的事情,特别是对于重复的“Literal True”,这非常不错。唯一仍然困扰我的是有些模式必须重复(And 的两侧都可以为 false),但也许我可以使用一些 TemplateHaskell 魔法。从未使用过那个,会很有趣! - Phil Kiener
你可以使用 simplify' (BoolInfix ifx a b) = simplify' (BoolInfix ifx b a) 条件语句来捕获所有内容;但是如果没有规则触发,那么你将会陷入无限循环。 - leftaroundabout
那对于非交换算子(例如蕴涵)也是不起作用的。 - Phil Kiener

12

你可以在构建过程中简化,而不是先构建然后反复拆除。所以:

module Simple (Expression, true, false, var, not, or, and) where

import Prelude hiding (not, or, and)

data Expression
    = Literal Bool
    | Variable String
    | Not Expression
    | Or Expression Expression
    | And Expression Expression
    deriving (Eq, Ord, Read, Show)

true = Literal True
false = Literal False
var = Variable

not (Literal True) = false
not (Literal False) = true
not x = Not x

or (Literal True) _ = true
or _ (Literal True) = true
or x y = Or x y

and (Literal False) _ = false
and _ (Literal False) = false
and x y = And x y

我们可以在ghci中试一下:

> and (var "x") (and (var "y") false)
Literal False

请注意构造函数没有被导出: 这确保了使用这些构造函数的人不能避免简化过程。实际上,这可能是一个缺点; 偶尔看到“完整”形式是很好的。处理这个问题的一个标准方法是将输出的智能构造函数作为类型类的一部分; 您可以使用它们来构建“完整”形式或“简化”的方式。为了避免定义类型两次,我们可以使用新类型或虚参; 我在这里选择后者以减少模式匹配中的噪声。

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
module Simple (Format(..), true, false, var, not, or, and) where

import Prelude hiding (not, or, and)

data Format = Explicit | Simplified

data Expression (a :: Format)
    = Literal Bool
    | Variable String
    | Not (Expression a)
    | Or (Expression a) (Expression a)
    | And (Expression a) (Expression a)
    deriving (Eq, Ord, Read, Show)

class Expr e where
    true, false :: e
    var :: String -> e
    not :: e -> e
    or, and :: e -> e -> e

instance Expr (Expression Explicit) where
    true = Literal True
    false = Literal False
    var = Variable
    not = Not
    or = Or
    and = And

instance Expr (Expression Simplified) where
    true = Literal True
    false = Literal False
    var = Variable

    not (Literal True) = false
    not (Literal False) = true
    not x = Not x

    or (Literal True) _ = true
    or _ (Literal True) = true
    or x y = Or x y

    and (Literal False) _ = false
    and _ (Literal False) = false
    and x y = And x y

现在在ghci中,我们可以用两种不同的方式“运行”相同的术语:

> :set -XDataKinds
> and (var "x") (and (var "y") false) :: Expression Explicit
And (Variable "x") (And (Variable "y") (Literal False))
> and (var "x") (and (var "y") false) :: Expression Simplified
Literal False

您可能希望稍后添加更多规则;例如,也许您想要:

and (Variable x) (Not (Variable y)) | x == y = false
and (Not (Variable x)) (Variable y) | x == y = false

需要重复两个“模式”的情况有点烦人。我们应该对其进行抽象!数据声明和类将保持不变,但我们将添加帮助函数eitherOrder并在andor的定义中使用它。以下是使用此想法的更完整的简化集(及我们模块的最终版本):

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
module Simple (Format(..), true, false, var, not, or, and) where

import Data.Maybe
import Data.Monoid
import Prelude hiding (not, or, and)
import Control.Applicative ((<|>))

data Format = Explicit | Simplified

data Expression (a :: Format)
    = Literal Bool
    | Variable String
    | Not (Expression a)
    | Or (Expression a) (Expression a)
    | And (Expression a) (Expression a)
    deriving (Eq, Ord, Read, Show)

class Expr e where
    true, false :: e
    var :: String -> e
    not :: e -> e
    or, and :: e -> e -> e

instance Expr (Expression Explicit) where
    true = Literal True
    false = Literal False
    var = Variable
    not = Not
    or = Or
    and = And

eitherOrder :: (e -> e -> e)
            -> (e -> e -> Maybe e)
            -> e -> e -> e
eitherOrder fExplicit fSimplified x y = fromMaybe
    (fExplicit x y)
    (fSimplified x y <|> fSimplified y x)

instance Expr (Expression Simplified) where
    true = Literal True
    false = Literal False
    var = Variable

    not (Literal True) = false
    not (Literal False) = true
    not (Not x) = x
    not x = Not x

    or = eitherOrder Or go where
        go (Literal True) _ = Just true
        go (Literal False) x = Just x
        go (Variable x) (Variable y) | x == y = Just (var x)
        go (Variable x) (Not (Variable y)) | x == y = Just true
        go _ _ = Nothing

    and = eitherOrder And go where
        go (Literal True) x = Just x
        go (Literal False) _ = Just false
        go (Variable x) (Variable y) | x == y = Just (var x)
        go (Variable x) (Not (Variable y)) | x == y = Just false
        go _ _ = Nothing

现在在ghci中,我们可以进行更复杂的简化操作,例如:

> and (not (not (var "x"))) (var "x") :: Expression Simplified
Variable "x"

即使我们只编写了一条重写规则,两个命令都可以正常工作:

> and (not (var "x")) (var "x") :: Expression Simplified
Literal False
> and (var "x") (not (var "x")) :: Expression Simplified
Literal False

1
在我的情况下,简化与否的选择将在构建术语之前进行,因此这将非常顺利。如果可以的话,我会因对称性处理方式再给你一个+1! - Phil Kiener

6
我认为爱因斯坦曾说过:“尽可能简化,但不要过度简化。”你有一个复杂的数据类型和相应的复杂概念,因此我认为任何技术都只能在处理问题时变得更加简洁。
话虽如此,第一种选择是使用 case 结构。
simplify x = case x of
   Literal _  -> x
   Variable _ -> x
   Not e      -> simplifyNot $ simplify e
   ...
   where
     sharedFunc1 = ...
     sharedFunc2 = ...

这样做的附加好处是包括了共享函数,这些函数可被所有用例使用,但不在顶层命名空间中。我也喜欢用例摆脱了它们的括号。 (请注意,在前两个用例中,我仅返回原始术语,而不创建新术语)。我经常使用这种结构来简化其他函数,例如Not的情况。
特别地,这个问题可能会借助于基于底层函数器的Expression,以便您可以对子表达式的简化进行fmap,然后执行给定用例的特定组合。 它看起来像以下内容:
simplify :: Expression' -> Expression'
simplify = Exp . reduce . fmap simplify . unExp

这个过程的步骤包括将Expression'展开为基础函子表示形式,将简化映射到基础项上,然后缩减该简化并重新包装成新的Expression'
{-# Language DeriveFunctor #-}

newtype Expression' = Exp { unExp :: ExpressionF Expression' }

data ExpressionF e
  = Literal Bool 
  | Variable String
  | Not e 
  | Or e e
  | And e e
  deriving (Eq,Functor)

现在,我已经把复杂性推到了“减少”(reduce)函数中,这个函数只是略微减少了一些复杂性,因为它不必再担心首先减少子项。但是,它现在仅包含将一个术语与另一个组合的业务逻辑。
这可能是对你有用的技巧,但它可能会使某些增强更容易。例如,如果在您的语言中可能会形成无效表达式,则可以使用Maybe值失败来简化该过程。
simplifyMb :: Expression' -> Maybe Expression'
simplifyMb = fmap Exp . reduceMb <=< traverse simplifyMb . unExp

在这里,traverse将应用于ExpressionF的子术语中的simplfyMb,导致Maybe子术语、ExpressionF (Maybe Expression')的表达式,如果任何子术语为Nothing,它将返回Nothing,如果所有子术语都是Just x,它将返回Just (e::ExpressionF Expression')。实际上,遍历并没有像那样分成不同的阶段,但假设它分开会更容易解释。还要注意,您需要在ExpressionF数据类型上使用DeriveTraversable和DeriveFoldable语言特性以及派生语句。
缺点呢?首先,您的代码中的污垢将存在于到处都是的Exp包装器中。考虑下面简单术语的simplfyMb应用:
simplifyMb (Exp $ Not (Exp $ Literal True))

这可能需要一些理解,但是如果你理解以上的traversefmap模式,你就可以在许多地方重复使用它,这很好。我还相信以那种方式定义简化会使它更具有鲁棒性,无论特定的ExpressionF构造如何变化,都不会受到影响。它没有提到它们,所以深度简化将不受重构的影响。另一方面,缩减函数则会受到影响。


很好。也许你可以说明第二种方法本质上是使用了recursion-schemes中的cata,以提供一个指针。 - chi
这确实是一种令人耳目一新的方法,因为表达式只是一棵可以遍历的树。但是,正如你所说的,将Exp随处传播确实让我感到困扰,我宁愿不这样做。 - Phil Kiener
2
@PhilippKiener,最近 GHC 版本中清理这个问题的常见方法是在 ExpressionF 构造函数的末尾添加 F 并使用模式同义词来定义 Expression' 的 "构造函数"。在这种情况下,它们是简单的双向模式同义词。例如:pattern And :: Expression' -> Expression' -> Expression'; pattern And a b = Exp (AndF a b) - dfeuer
这是一些有趣的建议,但在我看来,函数子类型实例没有意义。您正在滥用Functor/Traversable接口来表达一个(相当不规范的)高阶函数,最好只需将其作为简单的私有助手traverseSubexprs :: Applicative a => (Expression -> a Expression) -> Expression -> a Expression完成。实际上,一个真正的函数子类型应该能够合理、规范地包含任何类型(或者至少是某些明确定义的类中的任何类型,就像各种受限制的类别库所允许的那样),但在这里,只有Expression才有任何意义。 - leftaroundabout
@leftaroundabout,怎么了?这是一个非常常见的方法。为什么只有相同类型的子表达式对你有意义? - dfeuer
显示剩余4条评论

2

继续你的二元操作表达式表达式的想法,我们可以有以下数据类型:

data Expression
    = Literal Bool
    | Variable String
    | Not Expression
    | Binary Op Expression Expression
    deriving Eq

data Op = Or | And deriving Eq

辅助函数

以及一个辅助函数

{-# LANGUAGE ViewPatterns #-}

simplifyBinary  :: Op -> Expression -> Expression -> Expression
simplifyBinary  binop (simplify -> leftexp) (simplify -> rightexp) =
    case oneway binop leftexp rightexp ++ oneway binop rightexp leftexp of
        simplified : _ -> simplified
        []             -> Binary binop leftexp rightexp
  where
    oneway :: Op -> Expression -> Expression -> [Expression]
    oneway And (Literal False) _ = [Literal False]
    oneway Or  (Literal True)  _ = [Literal True]
    -- more cases here
    oneway _   _               _ = []

这个想法是将简化情况放在oneway中,然后simplifyBinary会处理反转参数的工作,以避免编写对称情况。

2

你可以编写一个通用的简化器来处理所有二进制操作:

simplifyBinWith :: (Bool -> Bool -> Bool) -- the boolean operation
                -> (Expression -> Expression -> Expression) -- the constructor
                -> Expression -> Expression -- the two operands
                -> Expression) -- the simplified result
simplifyBinWith op cons a b = case (simplify a, simplify b) of
    (Literal x, Literal y) -> Literal (op x y)
    (Literal x, b')        -> tryAll (x `op`) b'
    (a',        Literal y) -> tryAll (`op` y) a'
    (a',        b')        -> cons a' b'
  where
    tryAll f term = case (f True, f False) of -- what would f do if term was true of false
      (True,  True)  -> Literal True
      (True,  False) -> term
      (False, True)  -> Not term
      (False, False) -> Literal False

那么,您的简化函数将变为:
simplify :: Expression -> Expression
simplify (Not e)   = case simplify e of
    (Literal b) -> Literal (not b)
    e'          -> Not e'
simplify (And a b) = simplifyBinWith (&&) And a b
simplify (Or a b)  = simplifyBinWith (||) Or a b
simplify t         = t

这个方法可以很容易地扩展到更多的二进制操作。它也可以很好地与Binary Op Expression Expression的想法配合使用,您可以将Op传递给simplifyBinWith而不是Expression构造函数,并且simplify中的模式可以被泛化:

simplify :: Expression -> Expression
simplify (Not e)         = case simplify e of
    (Literal b) -> Literal (not b)
    e'          -> Not e'
simplify (Binary op a b) = simplifyBinWith (case op of
    And -> (&&)
    Or -> (||)
    Xor -> (/=)
    Implies -> (<=)
    Equals -> (==)
    …
  ) op a b
simplify t               = t
  where
    simplifyBinWith f op a b = case (simplify a, simplify b) of
      (Literal x, Literal y) -> Literal (f x y)
      …
      (a',        b')        -> Binary op a' b'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接