【问题标题】:Evaluating parsed expression in Haskell在 Haskell 中评估解析的表达式
【发布时间】:2026-02-08 02:55:01
【问题描述】:

这是我关于 SO 的第一个问题 :)

我的 Haskell 知识非常有限,所以我需要一些帮助才能开始。 我有这个 BNF 语法:

num ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
int ::= num | num int
var ::= A | B | C | ... | Z
expr ::= var | int | - expr
        | +(expr , expr) | *(expr , expr)
        | let var be expr in expr

我已经编写了一个解析器,在 SO 上的另一篇文章的帮助下。

我的数据类型是:

data Expr =  Var Char | Tall Int | Sum Expr Expr | Mult Expr Expr | Neg Expr | Let Expr  Expr Expr

我需要评估解析器输出的表达式(Expr、String)。我真的不知道从哪里开始这项任务。谁能帮帮我?

我不知道还需要什么信息,如果需要我会发布。

【问题讨论】:

标签: parsing haskell expression


【解决方案1】:

首先,在您的数据类型中,Let 构造函数的第一条数据应该只是变量标识符(在您的情况下为Char)而不是Expr

尝试递归函数。您将Expr 评估为Int,因此基本上您想要签名的功能

evaluate :: Expr -> Int

然后开始匹配 Expr 的构造函数并递归计算子表达式:

evaluate (Tall n) = n
evaluate (Sum e1 e2) = evaluate e1 + evaluate e2

当涉及Let 绑定和变量时,您需要扩展签名以另外传递将变量映射到其值的环境。这可以像(Char, Int) 对的列表一样简单。 Let 会将变量及其值添加到传递给 in 表达式的环境中。所以你最终会得到类似的东西:

evaluate :: Expr -> Int
evaluate e = evaluate' e EmptyEnv
  where evaluate' :: Expr -> Env -> Int
        evaluate' (Tall n) _ = n
        ...

当然,如果使用的变量未被let 绑定,则您必须提供错误处理。

这有帮助吗?

【讨论】:

  • 非常感谢您的回答!我有一个问题;我的解析函数返回(Expr,String),这会引发一个错误,只有 Expr 作为参数。有没有办法只使用元组的第一部分,还是我必须重写解析函数以只返回 Expr?
  • 您能否详细说明如何评估 let 表达式?我不明白我应该如何构建环境列表。
  • 我刚刚用处理 Env 的示例代码扩展了我的答案,见下文。
【解决方案2】:

环境

对于处理环境,可以使用

  • 来自 Prelude 的 lookup
  • 或其同名的 lookup 来自 Data.Map 库(以及库的其他功能)

如果你选择使用Data.Map模块,值得写

Data.Map.lookup

而不仅仅是lookup

或者——另一种解决方案——用

隐藏 Prelude 的lookup
import Prelude hiding (lookup)

这样就不会收到有关两个lookup 函数冲突的错误消息。

为简单起见,我首先使用 Prelude 的更简单的lookup 函数编写解决方案。

为了简单起见,我还没有加入错误处理。

环境:

module Env (Env) where

type Env = [Binding]

type Binding = (Char, Integer)

表达式:

module Expr where

data Expr = Var Char
          | Tall Int
          | Sum Expr Expr
          | Mult Expr Expr
          | Neg Expr
          | Let Char Expr Expr

评价:

module Semantics (evaluate) where

import Expr (Expr)
import Env (Env)

evaluate :: Expr -> Integer
evaluate = evaluate' []

evaluate' :: Env -> Expr -> Integer
evaluate' _   (Tall n) = n
evaluate' env (Var x) = case lookup x env of
    Just n -> n
    Nothing -> error ("Variable" ++ [x] ++ "is free!")
evaluate' env (Sum a b) = evaluate' env a + evaluate' env b
evaluate' env (Mult a b) = evaluate' env a * evaluate' env b
evaluate' env (Neg a) = - evaluate' env a
evaluate' env (Let x a b) = evaluate' ((x, a) : env) b

变量冲突

至于规划你的对象语言:在以后的版本中,值得规划一个策略,如何处理冲突的变量名:

let A be 5 in (A +3)

很清楚,但应该是什么意思

let A be 5 in (let A be 3 in A)

?

在您的评估器的早期版本中,您不必对此进行计划,因为lookup 函数将根据其定义中固有的默认行为“自动”决定情况。但是如果你不喜欢它的默认行为,你可能会用一个有意识的策略来增强你的评估器来处理变量名冲突。

错误处理

如果您在环境中计算表达式,并且表达式引用了环境中不包含的变量,那么解释器必须以某种方式报告错误。

你可以用几种方法,最简单的一种:

底部

使用error 函数,您可以强制程序停止并显示用户定义的错误消息。这个方案有缺点,但是写起来容易。

也许

您可以更改您的evaluate' 功能。它不会有签名

evaluate' :: Env -> Expr -> Integer

而是,而是

evaluate' :: Env -> Expr -> Maybe Integer

在这种情况下,你必须严格改变evaluate'的定义。你不能再写了:

evaluate' env (Sum a b) = evaluate' env a + evaluate' env b

但需要更复杂的定义

evaluate' env (Sum a b) = case (evaluate' env a, evaluate' env b) of
        (Just a0, Just b0) -> Just (a0 + b0)
        _                  -> Nothing

我们将 Maybe-ed 整数打包出来,对它们求和,然后将它们打包回 Maybe-ed 整数。这就像在“包内”进行求和。如果我们可以告诉 Haskell 它可以在 Maybe “内部”进行求和,我们可以节省很多工作。

如果我们利用 Maybe 是一个 monad,我们可以做到这一点:我们可以使用为处理 monad 而设计的函数。 Control.Monad 库中提供了此类辅助功能。在这里,liftM2 是一个函数,它可以帮助我们围绕 Maybe-packed 值进行求和:

evaluate' env (Sum a b) = liftM2 (+) (evaluate' env a) (evaluate' env b)

Data.Maybe 库中可以找到其他一些用于 Maybe-ed 值的辅助函数,但在这里,我们不需要它们。

module Semantics (evaluate) where

import Expr (Expr)
import Env (Env)
import Control.Monad (liftM, liftM2)

evaluate :: Expr -> Maybe Integer
evaluate = evaluate' []

evaluate' :: Env -> Expr -> Maybe Integer
evaluate' _   (Tall n) = Just n
evaluate' env (Var x) = lookup x env
evaluate' env (Sum a b) = liftM2 (+) (evaluate' env a) (evaluate' env b)
evaluate' env (Mult a b) = liftM2 (*) (evaluate' env a) (evaluate' env b)
evaluate' env (Neg a) = liftM negate (evaluate' env a)
evaluate' env (Let x a b) = evaluate' ((x, a) : env) b

【讨论】: