【问题标题】:Haskell: How to define my custom math data type recursively (stop infinite recursion)Haskell:如何递归定义我的自定义数学数据类型(停止无限递归)
【发布时间】:2018-09-10 10:33:46
【问题描述】:

我正在定义一个自定义数据类型来帮助我进行项目的微积分。我已将这种数据类型定义如下:

data Math a =       
            Add (Math a) (Math a)
        |   Mult (Math a) (Math a)
        |   Cos (Math a)
        |   Sin (Math a)
        |   Log (Math a)
        |   Exp (Math a)
        |   Const a
        |   Var Char

    deriving Show

我正在创建一个名为eval 的函数,它为我计算部分数学表达式。这就是我所拥有的:

eval (Const a) = (Const a)
eval (Var a) = (Var a)

eval (Add (Const a) (Const b)) = eval (Const (a+b))
eval (Add (Var a) b) = eval (Add (Var a) (eval b))
eval (Add a (Var b)) = eval (Add (eval a) (Var b))
eval (Add a b) = eval (Add (eval a) (eval b))

eval (Mult (Const a) (Const b)) = (Const (a*b))
eval (Mult a (Var b)) = (Mult (eval a) (Var b))
eval (Mult (Var a) b) = (Mult (Var a) (eval b))
eval (Mult a b) = eval (Mult (eval a) (eval b))

eval (Cos (Const a)) = (Const (cos(a)))
eval (Cos (Var a)) = (Cos (Var a))
eval (Cos a) = eval (Cos (eval a))

eval (Sin (Const a)) = (Const (sin(a)))
eval (Sin (Var a)) = (Sin (Var a))
eval (Sin a) = eval (Sin (eval a))

eval (Log (Const a)) = (Const (log(a)))
eval (Log (Var a)) = (Log (Var a))
eval (Log a) = eval (Log (eval a))

eval (Exp (Const a)) = (Const (exp(a)))
eval (Exp (Var a)) = (Exp (Var a))

这在大多数情况下都可以正常工作。例如,eval (Mult ((Const 4)) (Add (Cos (Const (0))) (Log (Const 1)))) 的结果是 (Const 4.0)

每当我添加一个带有两个常量的变量时,我的问题就会出现: eval (Add (Const 4) (Add (Const 4) (Var 'x'))) 给了我无限递归。我已经确定问题是因为我在eval (Add a b) = eval (Add (eval a) (eval b)) 上调用eval。如果我创建这条线 eval (Add a b) = (Add (eval a) (eval b)),我将停止无限递归,但我不再简化我的答案:Add (Const 4.0) (Add (Const 4.0) (Var 'x')) 得到完全相同的 Add (Const 4.0) (Add (Const 4.0) (Var 'x'))。我怎样才能得到类似Add (Const 8.0) (Var 'x') 的东西?

任何帮助将不胜感激!

【问题讨论】:

    标签: haskell recursion types functional-programming pattern-matching


    【解决方案1】:

    您的问题是您不会将已经简化的表达式与未简化的表达式区别对待。你一直调用eval,直到两个操作数都是常量,如果这种情况永远不会发生,你就永远不会终止。最简单的有问题的输入是Add (Var "x") (Const 5)。这样的输入应该结束递归并返回自身。但它会继续在同一输入上调用eval

      eval (Add (Var "x") (Const 5))
    = eval (Add (Var "x") (eval (Const 5)))
    = eval (Add (Var "x") (Const 5))
    = eval (Add (Var "x") (eval (Const 5)))
    = ... ad infinitum
    

    一般而言,首先避免此类问题的方法,即在您缺少基本情况时使其显而易见,是以这样一种方式构造您的函数,即您的函数的所有递归情况都调用自己仅适用于参数表达式的子表达式。

    在这种情况下,可以通过首先评估操作数然后对结果进行常量折叠而不需要再次递归调用来实现。看起来像这样:

    eval (Add a b) =
      case (eval a, eval b) of
        (Const x, Const y) => Const (x+y)
        (x, y) => Add x y
    

    这里唯一的递归调用是eval aeval b,其中ab 是原始表达式的子表达式。这保证了终止,因为如果所有情况都遵循此规则,您最终将到达没有子表达式的表达式,这意味着递归必须终止。

    【讨论】:

    • 我已经实现了它,它在某种程度上有所简化,但并不完全。例如,eval (Add (Const 10.0) (Add (Add (Const 4.0) (Var 'y')) (Var 'x'))) 只给出Add (Const 10.0) (Add (Add (Const 4.0) (Var 'y')) (Var 'x'))
    • @JackBuckley 这更复杂。实现这一点的一种方法是将一系列添加转换为列表,然后用单个常量替换所有常量。请注意,这种简化(与我们目前所做的不同)是特定于操作员的。例如,幂运算符2^(3^x) 已经是最简单的形式,无法进一步简化。我们可以将2+(3+x) 简化为5+x 的唯一原因是我们知道加法是一种关联操作。
    • @JackBuckley 另请注意,还有更多可以应用的优化(1*x == x+0 == x0*x = 02*x + 3*x == 5*x 等)。这只是你能想到多少关于运营商的规则,以及你想花多少时间来实施它们的问题。
    猜你喜欢
    • 2021-12-28
    • 1970-01-01
    • 2020-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-11
    相关资源
    最近更新 更多