简体   繁体   English

如何在Haskell中计算表达式

[英]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 -> Intx:: Char ,所以你可以只用fx来获取Int。 Pleasingly this will work across a collection of variable names. 令人满意的是,这将适用于变量名称的集合。

Example

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

Pass the assignment functions through to other calls of 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

Different order? 订单不同?

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解析变量名来躲避VarAssignment

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来避免显式匹配JustNothing

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. 这比你真正需要解决这个问题的信息要多得多,我只想提出一个替代解决方案,它使用MaybeEither ReaderT属性来提供简单的错误处理,并使用ReaderT来清除传递Assignment参数的噪音到处都是。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM