簡體   English   中英

如何在Haskell中計算表達式

[英]How to evaluate expressions in Haskell

我了解如何創建和評估簡單的數據類型Expr。 例如這樣:

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

所以這是我的問題:如何將變量添加到此Expr類型,應該針對其指定值進行評估? 它應該如下所示:

data Expr = Var Char | Lit Int | Add Expr Expr [...]
type Assignment = Char -> Int

eval :: Expr -> Assignment -> Int

我現在如何為(Var Char)和(Add Expr Expr)執行我的eval功能? 我想我已經找到了最簡單的,如何為Lit做這件事。

eval (Lit x) _ = x

對於(Var Char),我嘗試了很多,但是我無法從作業中獲得一個Int ..想想它會像這樣工作:

eval (Var x) (varname number) = number

您需要將賦值函數應用於變量名稱以獲取Int:

eval (Var x) f = f x

這是因為f :: Char -> Intx:: Char ,所以你可以只用fx來獲取Int。 令人滿意的是,這將適用於變量名稱的集合。

ass :: Assignment
ass 'a' = 1
ass 'b' = 2

意思是

eval ((Add (Var 'a') (Var 'b')) ass
= eval (Var 'a') ass + eval (Var 'b') ass
= ass 'a'            + ass 'b'
= 1 + 2
= 3

將賦值函數傳遞給其他eval調用

你需要繼續傳遞賦值函數,直到得到整數:

eval (Add x y) f = eval x f + eval y f

訂單不同?

如果允許更改類型,那么將賦值函數放在第一位且數據放在第二位似乎更合乎邏輯:

eval :: Assignment -> Expr -> Int
eval f (Var x) = f x
eval f (Add x y) = eval f x + eval f y

...但我想你可以把它看作一個帶有變量變量的常量表達式(感覺勢在必行),而不是一系列表達式中的一組常量值(感覺就像參考透明度)。

好吧,如果你把你的環境建模為

type Env = Char -> Int

那么你擁有的就是

eval (Var c) env = env c

但這不是真的“正確”。 首先,未綁定變量會發生什么? 所以也許更准確的類型

type Env = Char -> Maybe Int
emptyEnv = const Nothing

現在我們可以看到變量是否未綁定

eval (Var c) env = maybe handleUnboundCase id (env c)

現在我們可以使用handleUnboundCase來做一些事情,比如指定一個默認值,炸毀程序,或讓猴子爬出你的耳朵。

最后要問的問題是“變量如何約束?”。 如果您在Haskell中查找“let”語句,那么我們可以使用稱為HOAS(高階抽象語法)的技巧。

data Exp = ... | Let Exp (Exp -> Exp)

HOAS位是(Exp - > Exp)。 基本上我們使用Haskell的名稱綁定來實現我們的語言。 現在來評估我們做的let表達式

eval (Let val body) = body val

這讓我們通過依靠Haskell解析變量名來躲避VarAssignment

此樣式中的let語句示例可能是

 Let 1 $ \x -> x + x
 -- let x = 1 in x + x

這里最大的缺點是建模可變性是一種巨大的痛苦,但依賴於Assignment類型與具體地圖的情況就是這種情況。

我建議改用Data.Map Map 你可以實現類似的東西

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

但這看起來很笨重,每次添加操作時我們都必須繼續使用liftM2 op操作。 讓我們用一些助手清理一下吧

(|+|), (|-|), (|*|) :: (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

這是一個更好的,但我們仍然需要繞過各地的vars ,這對我來說是丑陋的。 相反,我們可以使用mtl包中的ReaderT monad。 ReaderT monad(和非變換器Reader )是一個非常簡單的monad,它公開了一個函數ask ,它返回你在運行時傳入的值,你可以做的就是“讀取”這個值,並且通常用於使用靜態配置運行應用程序。 在這種情況下,我們的“配置”是一個Assignment

這就是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

我們真正改變的唯一一件事就是每當遇到Var x時我們都要做一些額外的事情,我們刪除了vars參數。 我認為這使得evalM非常優雅,因為我們只在需要時訪問Assignment ,而且我們甚至不必擔心失敗,它完全由Monad實例為Maybe 在整個算法中沒有一行錯誤處理邏輯,但如果Assignment不存在變量名,它將優雅地返回Nothing


另外,考慮以后是否要切換到Double並添加除法,但是您還要返回錯誤代碼,以便確定是否存在除以0錯誤或查找錯誤。 而不是Maybe Double ,你可以使用Either ErrorCode Double

data ErrorCode
    = VarUndefinedError
    | DivideByZeroError
    deriving (Eq, Show, Read)

然后你可以把這個模塊寫成

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

現在我們確實有明確的錯誤處理,但它並不壞,我們已經能夠使用maybe來避免顯式匹配JustNothing

這比你真正需要解決這個問題的信息要多得多,我只想提出一個替代解決方案,它使用MaybeEither ReaderT屬性來提供簡單的錯誤處理,並使用ReaderT來清除傳遞Assignment參數的噪音到處都是。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM