【问题标题】:Reusing patterns in pattern guards or case expressions在模式保护或案例表达式中重用模式
【发布时间】:2014-10-08 07:48:11
【问题描述】:

我的 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 等的子表达式。

【问题讨论】:

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

标签: haskell pattern-matching pattern-guards


【解决方案1】:

这个答案的灵感来自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
    ...

我们需要添加对Varemap 的支持:

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 中描述了如何执行此操作。

【讨论】:

    【解决方案2】:

    我在类似情况下使用过的一个部分解决方案是将逻辑提取到“提升”函数中,该函数采用正常的 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) =&gt; Pack (a -&gt; b) 形式的实例。

    我不确定这是否适用于您的示例,但我希望它可以为您提供一个良好的起点。您可能希望在整个过程中连接额外的错误处理,但好消息是大部分都被合并到 packunpackliftOp 的定义中,所以它仍然非常集中。

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

    【讨论】:

      猜你喜欢
      • 2015-11-25
      • 2011-04-07
      • 2015-05-15
      • 1970-01-01
      • 2023-03-31
      • 1970-01-01
      • 2020-02-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多