Haskell 在模式保护或case表达式中重用模式

Haskell 在模式保护或case表达式中重用模式,haskell,pattern-matching,pattern-guards,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

我的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'
对我来说,这个定义看起来有点笨拙,所以我用模式保护重写了这个定义,如下所示:

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


编辑:在查看了图案防护装置后,我意识到它们在这里不能作为替代品。虽然当
x
y
可以减少到
I
时,它们提供相同的结果,但当图案保护不匹配时,它们不会减少任何值。我仍然希望
reduce
来简化
Add
等的子表达式。

我在类似情况下使用的一个部分解决方案是将逻辑提取到一个“提升”函数中,该函数接受一个正常的Haskell操作,并将其应用于您的语言值。这抽象了包装/展开以及由此产生的错误处理

我们的想法是创建两个TypeClass,用于进出自定义类型,并进行适当的错误处理。然后,您可以使用这些创建一个
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)

我不确定这对您的示例是否完全有效,但我希望它能为您提供一个良好的起点。您可能希望在整个过程中连接额外的错误处理,但好消息是,大部分错误处理都被合并到
pack
unpack
liftOp
的定义中,因此它仍然相当集中


我为一个相关(但有些不同)的问题提供了类似的解决方案。这也是处理原生Haskell值和解释器之间来回的一种方法,但是解释器的结构不同。但是,一些相同的想法仍然应该适用

这个答案的灵感来自,它暗示了以下功能:

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
步骤
查看单个术语,如果存在减少该术语所需的一切,则减少该术语。配备了
步骤
,我们只需要一种方法来替换表达式树中任何地方的术语。我们可以从定义在每个术语中应用函数的方法开始

{-# 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
的可能性,我将其称为
shorten
reduce

shorten = premap step
reduce = postmap step
这些行为有点不同<代码>缩短删除最内层的术语,将其替换为文字,将表达式树的高度缩短1
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
可以用
应用程序
函子
编写,并且
postmap
可以制作成一段通用代码,适用于表达式以外的其他数据类型。有关如何执行此操作的说明,请参见。

对于您的代码本身,reduce的类型是
Expression a->Expression Int
?这可能是使用@rampion的情况,谢谢,我听说过uniplate,但对它知之甚少。我想现在是深入研究泛型编程的好时机!你可以做的另一件事,虽然我不确定你是否愿意,是用类似于
数据Exp a where BinaryOp::(a->a->a)->(Exp a)->(Exp a)->(Exp a)
等的东西替换你当前的函数。@guhou是个天真的人。