重复使用模式保护或案例表达中的模式

时间:2014-08-14 15:22:46

标签: haskell pattern-matching pattern-guards

我的Haskell项目包含一个表达式求值程序,出于此问题的目的,可以简化为:

data Expression a where
    I :: Int -> Expression Int
    B :: Bool -> Expression Bool
    Add :: Expression Int  -> Expression Int  -> Expression Int
    Mul :: Expression Int  -> Expression Int  -> Expression Int
    Eq  :: Expression Int  -> Expression Int  -> Expression Bool
    And :: Expression Bool -> Expression Bool -> Expression Bool
    Or  :: Expression Bool -> Expression Bool -> Expression Bool
    If  :: Expression Bool -> Expression a    -> Expression a -> Expression a

-- Reduces an Expression down to the simplest representation.
reduce :: Expression a -> Expression a
-- ... implementation ...

实现此目的的直接方法是编写case表达式以递归计算和模式匹配,如下所示:

reduce (Add x y) = case (reduce x, reduce y) of
                    (I x', I y') -> I $ x' + y'
                    (x', y')     -> Add x' y'
reduce (Mul x y) = case (reduce x, reduce y) of
                    (I x', I y') -> I $ x' * y'
                    (x', y')     -> Mul x' y'
reduce (And x y) = case (reduce x, reduce y) of
                    (B x', B y') -> B $ x' && y'
                    (x', y')     -> And x' y'
-- ... and similarly for other cases.

对我来说,这个定义看起来有点尴尬,所以我接着用模式保护重写了这个定义,如下:

reduce (Add x y) | I x' <- reduce x
                 , I y' <- reduce y
                 = I $ x' + y'

我认为这个定义与case表达式相比看起来更干净,但是当为不同的构造函数定义多个模式时,模式会重复多次。

reduce (Add x y) | I x' <- reduce x
                 , I y' <- reduce y
                 = I $ x' + y'
reduce (Mul x y) | I x' <- reduce x
                 , I y' <- reduce y
                 = I $ x' * y'

注意到这些重复的模式,我希望有一些语法或结构可以减少模式匹配中的重复。是否有一种普遍接受的方法来简化这些定义?

编辑:在审核模式保护后,我意识到它们不能作为替代品在这里工作。虽然xy可以缩减为I _时提供相同的结果,但当模式保护不匹配时,它们不会减少任何值。我仍然希望reduce简化Add et al。

的子表达式

2 个答案:

答案 0 :(得分:8)

我在类似情况下使用的一个部分解决方案是将逻辑提取到“提升”函数中,该函数采用正常的Haskell操作并将其应用于语言的值。这包括了包装/解包和导致的错误处理。

我们的想法是创建两个类型类,以便进出自定义类型,并进行适当的错误处理。然后你可以使用它们来创建一个看起来像这样的liftOp函数:

liftOp :: (Extract a, Extract b, Pack c) => (a -> b -> c) -> 
            (Expression a -> Expression b -> Expression c)
liftOp err op a b = case res of
  Nothing  -> err a' b'
  Just res -> pack res
  where res = do a' <- extract $ reduce' a
                 b' <- extract $ reduce' b
                 return $ a' `op` b'

然后每个具体案例如下:

Mul x y -> liftOp Mul (*) x y

这不是太糟糕:它不是过分冗余。它包含重要的信息:Mul映射到*,在错误情况下,我们只是再次应用Mul

您还需要打包和解包的实例,但无论如何这些都很有用。一个巧妙的技巧是,这些也可以让你自动在DSL中嵌入函数,使用(Extract a, Pack b) => Pack (a -> b)形式的实例。

我不确定这是否适用于您的示例,但我希望它能为您提供一个良好的起点。您可能希望通过整个事情连接其他错误处理,但好消息是大多数错误处理都被放入packunpackliftOp的定义中,所以它仍然很漂亮集中。

I wrote up针对相关(但有些不同)问题的类似解决方案。它也是一种处理本机Haskell值和解释器之间来回的方法,但解释器的结构不同。尽管如此,一些相同的想法仍然适用!

答案 1 :(得分:2)

这个答案的灵感来自rampion's follow-up question,它提出了以下功能:

step :: Expression a -> Expression a
step x = case x of
  Add (I x) (I y) -> I $ x + y
  Mul (I x) (I y) -> I $ x * y
  Eq  (I x) (I y) -> B $ x == y
  And (B x) (B y) -> B $ x && y
  Or  (B x) (B y) -> B $ x || y
  If  (B b) x y   -> if b then x else y
  z               -> z

step查看单个术语,如果存在减少所需的所有内容,则会减少它。配备step,我们只需要一种方法来替换表达式树中的任何地方的术语。我们可以从定义一种在每个术语中应用函数的方法开始。

{-# LANGUAGE RankNTypes #-}

emap :: (forall a. Expression a -> Expression a) -> Expression x -> Expression x
emap f x = case x of
    I a -> I a
    B a -> B a
    Add x y   -> Add (f x) (f y)
    Mul x y   -> Mul (f x) (f y)
    Eq  x y   -> Eq  (f x) (f y)
    And x y   -> And (f x) (f y)
    Or  x y   -> Or  (f x) (f y)
    If  x y z -> If  (f x) (f y) (f z)

现在,我们需要在任何地方应用一个函数,无论是术语还是术语中的任何地方。有两种基本的可能性,我们可以在将函数应用到术语之前将其应用于术语,或者我们可以在之后应用函数。

premap :: (forall a. Expression a -> Expression a) -> Expression x -> Expression x
premap f = emap (premap f) . f

postmap :: (forall a. Expression a -> Expression a) -> Expression x -> Expression x
postmap f = f . emap (postmap f)

这为我们提供了两种使用step的可能性,我将其称为shortenreduce

shorten = premap step
reduce = postmap step

这些行为略有不同。 shorten删除最内层的术语,用文字替换它们,将表达式树的高度缩短一。 reduce将表达式树完全评估为文字。这是在同一输入上迭代每个输入的结果

"shorten"
If (And (B True) (Or (B False) (B True))) (Add (I 1) (Mul (I 2) (I 3))) (I 0)
If (And (B True) (B True)) (Add (I 1) (I 6)) (I 0)
If (B True) (I 7) (I 0)
I 7
"reduce"
If (And (B True) (Or (B False) (B True))) (Add (I 1) (Mul (I 2) (I 3))) (I 0)
I 7

部分减少

您的问题暗示您有时会期望表达式无法完全缩小。我将通过添加变量Var来扩展您的示例,以包含一些内容来演示此案例。

data Expression a where
    Var :: Expression Int
    ...

我们需要将Var的支持添加到emap

emap f x = case x of
   Var -> Var
   ...

bind将替换变量,evaluateFor执行完整评估,仅遍历表达式一次。

bind :: Int -> Expression a -> Expression a
bind a x = case x of
    Var -> I a
    z   -> z

evaluateFor :: Int -> Expression a -> Expression a
evaluateFor a = postmap (step . bind a)

现在reduce迭代包含变量的示例会产生以下输出

"reduce"
If (And (B True) (Or (B False) (B True))) (Add (I 1) (Mul Var (I 3))) (I 0)
Add (I 1) (Mul Var (I 3))

如果针对特定值Var评估缩减的输出表达式,我们可以将表达式一直缩减为文字。

"evaluateFor 5"
Add (I 1) (Mul Var (I 3))
I 16

应用型

emap可以用Applicative Functor来编写,postmap可以制作成适用于除表达式之外的其他数据类型的通用代码。 this answer to rampion's follow-up question中描述了如何执行此操作。