[英]How to evaluate expressions in Haskell
I understand how to create and evaluate a simple data-type Expr. 我了解如何创建和评估简单的数据类型Expr。 For example like this: 例如这样:
data Expr = Lit Int | Add Expr Expr | Sub Expr Expr | [...]
eval :: Expr -> Int
eval (Lit x) = x
eval (Add x y) = eval x + eval y
eval (Sub x y) = eval x - eval y
So here is my question: How can I add Variables to this Expr type, which should be evaluated for its assigned value? 所以这是我的问题:如何将变量添加到此Expr类型,应该针对其指定值进行评估? It should look like this: 它应该如下所示:
data Expr = Var Char | Lit Int | Add Expr Expr [...]
type Assignment = Char -> Int
eval :: Expr -> Assignment -> Int
How do I have to do my eval function now for (Var Char) and (Add Expr Expr)? 我现在如何为(Var Char)和(Add Expr Expr)执行我的eval功能? I think I figured out the easiest, how to do it for Lit already. 我想我已经找到了最简单的,如何为Lit做这件事。
eval (Lit x) _ = x
For (Var Char) I tried a lot, but I cant get an Int out of an Assignment.. Thought It would work like this: 对于(Var Char),我尝试了很多,但是我无法从作业中获得一个Int ..想想它会像这样工作:
eval (Var x) (varname number) = number
You need to apply your Assignment function to the variable name to get the Int: 您需要将赋值函数应用于变量名称以获取Int:
eval (Var x) f = f x
This works because f :: Char -> Int
and x:: Char
, so you can just do fx
to get an Int. 这是因为f :: Char -> Int
和x:: Char
,所以你可以只用fx
来获取Int。 Pleasingly this will work across a collection of variable names. 令人满意的是,这将适用于变量名称的集合。
ass :: Assignment
ass 'a' = 1
ass 'b' = 2
meaning that 意思是
eval ((Add (Var 'a') (Var 'b')) ass
= eval (Var 'a') ass + eval (Var 'b') ass
= ass 'a' + ass 'b'
= 1 + 2
= 3
eval
将赋值函数传递给其他eval
调用 You need to keep passing the assignment function around until you get integers: 你需要继续传递赋值函数,直到得到整数:
eval (Add x y) f = eval x f + eval y f
If you're allowed to change the types, it seems more logical to me to put the assignment function first and the data second: 如果允许更改类型,那么将赋值函数放在第一位且数据放在第二位似乎更合乎逻辑:
eval :: Assignment -> Expr -> Int
eval f (Var x) = f x
eval f (Add x y) = eval f x + eval f y
...but I guess you can think of it as a constant expression with varying variables (feels imperative)rather than a constant set of values across a range of expressions (feels like referential transparency). ...但我想你可以把它看作一个带有变量变量的常量表达式(感觉势在必行),而不是一系列表达式中的一组常量值(感觉就像参考透明度)。
Well if you model your enviroment as 好吧,如果你把你的环境建模为
type Env = Char -> Int
Then all you have is 那么你拥有的就是
eval (Var c) env = env c
But this isn't really "correct". 但这不是真的“正确”。 For one, what happens with unbound variables? 首先,未绑定变量会发生什么? So perhaps a more accurate type is 所以也许更准确的类型
type Env = Char -> Maybe Int
emptyEnv = const Nothing
And now we can see whether a variable is unbound 现在我们可以看到变量是否未绑定
eval (Var c) env = maybe handleUnboundCase id (env c)
And now we can use handleUnboundCase
to do something like assign a default value, blow up the program, or make monkeys climb out of your ears. 现在我们可以使用handleUnboundCase
来做一些事情,比如指定一个默认值,炸毁程序,或让猴子爬出你的耳朵。
The final question to ask is "how are variables bound?". 最后要问的问题是“变量如何约束?”。 If you where looking for a "let" statement like we have in Haskell, then we can use a trick known as HOAS (higher order abstract syntax). 如果您在Haskell中查找“let”语句,那么我们可以使用称为HOAS(高阶抽象语法)的技巧。
data Exp = ... | Let Exp (Exp -> Exp)
The HOAS bit is that (Exp -> Exp). HOAS位是(Exp - > Exp)。 Essentially we use Haskell's name-binding to implement our languages. 基本上我们使用Haskell的名称绑定来实现我们的语言。 Now to evaluate a let
expression we do 现在来评估我们做的let
表达式
eval (Let val body) = body val
This let's us dodge Var
and Assignment
by relying on Haskell to resolve the variable name. 这让我们通过依靠Haskell解析变量名来躲避Var
和Assignment
。
An example let statement in this style might be 此样式中的let语句示例可能是
Let 1 $ \x -> x + x
-- let x = 1 in x + x
The biggest downside here is that modelling mutability is a royal pain, but this was already the case when relying on the Assignment
type vs a concrete map. 这里最大的缺点是建模可变性是一种巨大的痛苦,但依赖于Assignment
类型与具体地图的情况就是这种情况。
I would recommend using Map
from Data.Map
instead. 我建议改用Data.Map
Map
。 You could implement it something like 你可以实现类似的东西
import Data.Map (Map)
import qualified Data.Map as M -- A lot of conflicts with Prelude
-- Used to map operations through Maybe
import Control.Monad (liftM2)
data Expr
= Var Char
| Lit Int
| Add Expr Expr
| Sub Expr Expr
| Mul Expr Expr
deriving (Eq, Show, Read)
type Assignment = Map Char Int
eval :: Expr -> Assignment -> Maybe Int
eval (Lit x) _ = Just x
eval (Add x y) vars = liftM2 (+) (eval x vars) (eval y vars)
eval (Sub x y) vars = liftM2 (-) (eval x vars) (eval y vars)
eval (Mul x y) vars = liftM2 (*) (eval x vars) (eval y vars)
eval (Var x) vars = M.lookup x vars
But this looks clunky, and we'd have to keep using liftM2 op
every time we added an operation. 但这看起来很笨重,每次添加操作时我们都必须继续使用liftM2 op
操作。 Let's clean it up a bit with some helpers 让我们用一些助手清理一下吧
(|+|), (|-|), (|*|) :: (Monad m, Num a) => m a -> m a -> m a
(|+|) = liftM2 (+)
(|-|) = liftM2 (-)
(|*|) = liftM2 (*)
infixl 6 |+|, |-|
infixl 7 |*|
eval :: Expr -> Assignment -> Maybe Int
eval (Lit x) _ = return x -- Use generic return instead of explicit Just
eval (Add x y) vars = eval x vars |+| eval y vars
eval (Sub x y) vars = eval x vars |-| eval y vars
eval (Mul x y) vars = eval x vars |*| eval y vars
eval (Var x) vars = M.lookup x vars
That's a better, but we still have to pass around the vars
everywhere, this is ugly to me. 这是一个更好的,但我们仍然需要绕过各地的vars
,这对我来说是丑陋的。 Instead, we can use the ReaderT
monad from the mtl package. 相反,我们可以使用mtl包中的ReaderT
monad。 The ReaderT
monad (and the non-transformer Reader
) is a very simple monad, it exposes a function ask
that returns the value you pass in when it's run, where all you can do is "read" this value, and is usually used for running an application with static configuration. ReaderT
monad(和非变换器Reader
)是一个非常简单的monad,它公开了一个函数ask
,它返回你在运行时传入的值,你可以做的就是“读取”这个值,并且通常用于使用静态配置运行应用程序。 In this case, our "config" is an Assignment
. 在这种情况下,我们的“配置”是一个Assignment
。
This is where the liftM2
operators really come in handy 这就是liftM2
运营商真正派上用场的地方
-- This is a long type signature, let's make an alias
type ExprM a = ReaderT Assignment Maybe a
-- Eval still has the same signature
eval :: Expr -> Assignment -> Maybe Int
eval expr vars = runReaderT (evalM expr) vars
-- evalM is essentially our old eval function
evalM :: Expr -> ExprM Int
evalM (Lit x) = return x
evalM (Add x y) = evalM x |+| evalM y
evalM (Sub x y) = evalM x |-| evalM y
evalM (Mul x y) = evalM x |*| evalM y
evalM (Var x) = do
vars <- ask -- Get the static "configuration" that is our list of vars
lift $ M.lookup x vars
-- or just
-- evalM (Var x) = ask >>= lift . M.lookup x
The only thing that we really changed was that we have to do a bit extra whenever we encounter a Var x
, and we removed the vars
parameter. 我们真正改变的唯一一件事就是每当遇到Var x
时我们都要做一些额外的事情,我们删除了vars
参数。 I think this makes evalM
very elegant, since we only access the Assignment
when we need it, and we don't even have to worry about failure, it's completely taken care of by the Monad
instance for Maybe
. 我认为这使得evalM
非常优雅,因为我们只在需要时访问Assignment
,而且我们甚至不必担心失败,它完全由Monad
实例为Maybe
。 There isn't a single line of error handling logic in this entire algorithm, yet it will gracefully return Nothing
if a variable name is not present in the Assignment
. 在整个算法中没有一行错误处理逻辑,但如果Assignment
不存在变量名,它将优雅地返回Nothing
。
Also, consider if later you wanted to switch to Double
s and add division, but you also want to return an error code so you can determine if there was a divide by 0 error or a lookup error. 另外,考虑以后是否要切换到Double
并添加除法,但是您还要返回错误代码,以便确定是否存在除以0错误或查找错误。 Instead of Maybe Double
, you could use Either ErrorCode Double
where 而不是Maybe Double
,你可以使用Either ErrorCode Double
data ErrorCode
= VarUndefinedError
| DivideByZeroError
deriving (Eq, Show, Read)
Then you could write this module as 然后你可以把这个模块写成
data Expr
= Var Char
| Lit Double
| Add Expr Expr
| Sub Expr Expr
| Mul Expr Expr
| Div Expr Expr
deriving (Eq, Show, Read)
type Assignment = Map Char Double
data ErrorCode
= VarUndefinedError
| DivideByZeroError
deriving (Eq, Show, Read)
type ExprM a = ReaderT Assignment (Either ErrorCode) a
eval :: Expr -> Assignment -> Either ErrorCode Double
eval expr vars = runReaderT (evalM expr) vars
throw :: ErrorCode -> ExprM a
throw = lift . Left
evalM :: Expr -> ExprM Double
evalM (Lit x) = return x
evalM (Add x y) = evalM x |+| evalM y
evalM (Sub x y) = evalM x |-| evalM y
evalM (Mul x y) = evalM x |*| evalM y
evalM (Div x y) = do
x' <- evalM x
y' <- evalM y
if y' == 0
then throw DivideByZeroError
else return $ x' / y'
evalM (Var x) = do
vars <- ask
maybe (throw VarUndefinedError) return $ M.lookup x vars
Now we do have explicit error handling, but it isn't bad, and we've been able to use maybe
to avoid explicitly matching on Just
and Nothing
. 现在我们确实有明确的错误处理,但它并不坏,我们已经能够使用maybe
来避免显式匹配Just
和Nothing
。
This is a lot more information than you really need to solve this problem, I just wanted to present an alternative solution that uses the monadic properties of Maybe
and Either
to provide easy error handling and use ReaderT
to clean up that noise of passing an Assignment
argument around everywhere. 这比你真正需要解决这个问题的信息要多得多,我只想提出一个替代解决方案,它使用Maybe
和Either
ReaderT
属性来提供简单的错误处理,并使用ReaderT
来清除传递Assignment
参数的噪音到处都是。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.