【问题标题】:Haskell: recursion from leafs to rootHaskell:从叶子到根的递归
【发布时间】:2018-03-20 18:52:00
【问题描述】:

我有一个初等数学算术表达式的 AST:

data Expr = Constant Int
          | Variable String
          | Add Expr Expr
          | Multiply Expr Expr
          deriving (Show)

我还有一个非常简单的函数可以简化给定的表达式:

simplify :: Expr -> Expr
simplify (Add (Constant 0) e) = simplify e
simplify (Add e (Constant 0)) = simplify e
simplify (Add (Constant a) (Constant b)) = Constant (a + b)
simplify (Add e1 e2) = Add (simplify e1) (simplify e2)
simplify (Multiply (Constant 0) _) = Constant 0
simplify (Multiply _ (Constant 0)) = Constant 0
simplify (Multiply (Constant 1) e) = e
simplify (Multiply e (Constant 1)) = e
simplify (Multiply (Constant a) (Constant b)) = Constant (a * b)
simplify (Multiply e1 e2) = Multiply (simplify e1) (simplify e2)
simplify e = e

不幸的是,这个函数不是很有效,因为它简化了从根到叶(从上到下)的表达。考虑这个表达式:

exampleExpr :: Expr
exampleExpr = Add
                (Multiply (Constant 1) (Variable "redrum"))
                (Multiply (Constant 0) (Constant 451))

需要两次函数调用 (simplify (simplify exampleExpr)) 才能将此表达式简化为 Variable "redrum"。使用自下而上的方法,它应该只需要一次函数调用。

我还没有足够的经验来有效地编写此代码。所以我的问题是:如何重写这个函数来简化从叶子到根(从下到上)的给定表达式?

【问题讨论】:

    标签: haskell


    【解决方案1】:

    首先,您错过了几个递归调用。在这些行中:

    simplify (Multiply (Constant 1) e) = e
    simplify (Multiply e (Constant 1)) = e
    

    您应该将右侧替换为simplify e

    simplify (Multiply (Constant 1) e) = simplify e
    simplify (Multiply e (Constant 1)) = simplify e
    

    现在从下往上重写表达式。问题是您正在寻找等式左侧的简化模式,即在您简化孩子之前。您需要先简化孩子,然后再寻找模式。

    simplify :: Expr -> Expr
    simplify (Add x y) =
        case (simplify x, simplify y) of
            (Constant 0, e) -> e
            (e, Constant 0) -> e
            (Constant a, Constant b) -> Constant (a + b)
            (x1, y1) -> Add x1 y1
    simplify (Multiply x y) =
        case (simplify x, simplify y) of
            (Constant 0, _) -> Constant 0
            (_, Constant 0) -> Constant 0
            (Constant 1, e) -> e
            (e, Constant 1) -> e
            (Constant a, Constant b) -> Constant (a * b)
            (x1, y1) -> Multiply x1 y1
    simplify e = e
    

    在等式的左边,我们找到当前节点的子节点。在右边,我们在简化的孩子中寻找模式。改进此代码的一种方法是将查找和替换子项和匹配简化模式这两个职责分开。这是递归替换Expr 的每个子树的通用函数:

    transform :: (Expr -> Expr) -> Expr -> Expr
    transform f (Add x y) = f $ Add (transform f x) (transform f y)
    transform f (Multiply x y) = f $ Multiply (transform f x) (transform f y)
    transform f e = f e
    

    transform 采用(非递归)转换函数计算单节点模式的替换,并以自下而上的方式递归地将其应用于树中的每个节点。要编写转换函数,您只需寻找有趣的模式,而无需递归地重写子函数。

    simplify = transform f
        where
            f (Add (Constant 0) e) = e
            f (Add e (Constant 0)) = e
            f (Add (Constant a) (Constant b)) = Constant (a + b)
            f (Multiply (Constant 0) _) = Constant 0
            f (Multiply _ (Constant 0)) = Constant 0
            f (Multiply (Constant 1) e) = e
            f (Multiply e (Constant 1)) = e
            f (Multiply (Constant a) (Constant b)) = Constant (a * b)
            f e = e
    

    由于f 的参数已经用transform 重写了它的子参数,所以我们不需要穷举匹配所有可能的模式或显式地递归该值。我们寻找我们关心的那些,而不需要转换的节点则落入包罗万象的f e = e 案例中。

    lensPlated module 这样的通用编程库采用像transform 这样的编程模式并使它们具有通用性。您(或编译器)编写少量代码来表征您的数据类型的形状,并且该库一劳永逸地实现了递归高阶函数,例如 transform

    【讨论】:

    • 谢谢,我忘记了(编辑了我的答案)。但是,仍然需要两次调用才能完全简化表达式。
    • 啊,是的,很抱歉,我明白了问题所在。您需要在寻找模式之前简化孩子。我会更新答案,请耐心等待......
    • 它工作正常,非常感谢您抽出宝贵时间!这个答案很完美。
    • 乐于助人 ?
    • 这看起来可能是递归方案的一个很好的应用(特别是cata)。我不确定Plated 的全部功能在这里是否必要或有帮助。
    【解决方案2】:

    简化表达式 AST 是递归方案的典型应用,称为 catamorphism。以下是 Edwald Kmett 的 recursion-schemes 库示例:

    {-# LANGUAGE TypeFamilies #-}
    {-# LANGUAGE DeriveFunctor #-} 
    {-# LANGUAGE DeriveFoldable #-} 
    {-# LANGUAGE DeriveTraversable #-}
    {-# LANGUAGE TemplateHaskell #-}
    
    module CataExprSimplify where
    
    import Data.Functor.Foldable
    import Data.Functor.Foldable.TH
    
    data Expr = Constant Int
              | Variable String
              | Add Expr Expr
              | Multiply Expr Expr
              deriving (Show)
    
    -- | Generate the base functor
    makeBaseFunctor ''Expr 
    
    simplify :: Expr -> Expr 
    simplify = cata $ algSimplAdd . project . algSimplMult
    
    -- | Simplify Addition
    simplZero :: Expr -> Expr 
    simplZero = cata algSimplAdd
    
    algSimplAdd :: ExprF Expr -> Expr 
    algSimplAdd (AddF (Constant 0) r) = r 
    algSimplAdd (AddF l (Constant 0)) = l 
    algSimplAdd (AddF (Constant l) (Constant r)) = Constant (l + r)
    algSimplAdd x  = embed x
    
    -- | Simplify Multiplication
    simplMult :: Expr -> Expr 
    simplMult = cata algSimplMult
    
    algSimplMult :: ExprF Expr -> Expr 
    algSimplMult (MultiplyF (Constant 1) r) = r
    algSimplMult (MultiplyF l (Constant 1)) = l 
    algSimplMult (MultiplyF (Constant 0) _) = Constant 0
    algSimplMult (MultiplyF _ (Constant 0)) = Constant 0 
    algSimplMult (MultiplyF (Constant l) (Constant r)) = Constant (l * r)
    algSimplMult x = embed x
    

    与使用直接递归调用的代码相比,它具有以下优点:

    • 递归被抽象为 cata 函数,而不与您的简化逻辑交织在一起。
    • 您不会忘记对子表达式调用简化。
    • 变质作用自下而上。
    • 加法和乘法的简化可以写在单独的函数中。
    • 如果您必须扩展 AST(例如添加新的构造函数),维护代码会容易得多

    如果您想了解有关递归方案的更多信息,请阅读此blog post series

    【讨论】:

    • 谢谢。这个答案真的很有帮助。另外,我终于知道什么是变质了。
    • 值得注意的是,来自my answertransformcata 的一个特例(因为ExprF Expr ~~ Expr)。练习:根据cata实现transform
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-02-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-13
    • 1970-01-01
    相关资源
    最近更新 更多