根据评论,不要尝试将此作为您的第一个口译员。如果您还没有为非类型化 lambda 演算编写解释器或完成教程,例如48小时内给自己写一个计划 https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours,先这样做。
无论如何,这是一种静态类型表达式语言的解释器的简单实现,具有布尔和数字类型、一些内置运算符(包括具有临时多态性的运算符)、变量和let x=... in ...
变量绑定,但没有 lambda。它说明了设计类型化解释器的常见方法,但缺少足够的内容,因此不会破坏您的乐趣。
注意:我有意避免使用任何中级或高级 Haskell 功能(例如,类型ExprU
and ExprT
没有统一为单一的多态类型——不”生长的树 http://www.jucs.org/jucs_23_1/trees_that_grow/jucs_23_01_0042_0062_najd.pdf“对于我们来说!;我还没有使用 GADT利用 Haskell 类型系统 https://hackage.haskell.org/package/glambda输入目标语言; ETC。)。这些先进的技术可以产生更安全的代码——而且也非常棒,所以你将来肯定想看看它们——但它们并不是让基本类型解释器工作所必需的。
编写解释器时,最好打开-Wall
-- 它会提醒您忘记处理哪些模式(即表达式类型):
{-# OPTIONS_GHC -Wall #-}
另外,为了保持理智,我们需要使用一些 monad:
import Control.Monad
import Control.Monad.Reader
import Control.Monad.Except
您在问题中提到,您正在努力解决两种方法:一开始就按类型划分运算符,而不是以某种方式反映 Haskell 类型系统中的运算符类型。你对第一种方法的直觉是正确的——它的效果不会很好。第二种方法是可行的,但是您很快就会遇到一些非常先进的 Haskell 技术,而您可能还没有准备好。
相反,对于我们的静态类型语言,让我们首先定义一个完整的untyped抽象表达式语法。请注意,这是一种可能由完全不了解类型的解析器生成的抽象语法:
-- Untyped expressions
data ExprU
= FalseU | TrueU -- boolean literals
| NumU Double -- numeric literal
| VarU String -- variable
| UnU UnOp ExprU -- unary operator
| BinU BinOp ExprU ExprU -- binary operator
| LetU String ExprU ExprU -- let x = expr1 in expr2
data UnOp = NegOp | NotOp
deriving (Show)
data BinOp = PlusOp | MulOp | AndOp | OrOp | EqualsOp
deriving (Show)
请注意,我们可以直接为此编写一个解释器。然而,解释器必须处理类型错误的表达式,例如:
BinU PlusOp FalseU (NumU 1) -- False + 1
这会破坏定义静态类型语言的全部目的。
关键的见解是,我们可以采用这种非类型化语言,在解释它之前,实际上类型检查它!使用 Haskell 类型系统对目标语言进行类型检查有很酷的技术,但定义一个单独的数据类型来表示表达式类型要容易得多:
-- Simple expression types
data Typ
= BoolT
| NumT
deriving (Show, Eq)
对于我们的操作员来说,给他们“类型”也会很方便:
-- Types of operators
data BinTyp = BinTyp Typ Typ Typ -- binary ops: two arg types plus result type
data UnTyp = UnTyp Typ Typ -- unary ops: arg type plus result type
在具有一流函数的语言中,我们可能会将这些类型组合成一个 HaskellTyp
它不仅可以表示 bool 和 nums 等“原始”类型,还可以表示函数类型,例如Bool -> Bool -> Bool
, 等等。但是,对于这种简单的语言,我们将“表达式类型”和“运算符类型”分开。
我们如何处理这些类型?好吧,我们采用非类型化表达式ExprU
并通过向每个表达式添加类型注释来对它们进行类型检查:
-- Typed expressions
data ExprT
= BoolLit Bool
| NumLit Double
| VarT Typ String
| UnT Typ UnOp ExprT
| BinT Typ BinOp ExprT ExprT
| LetT Typ String ExprT ExprT
在这里,每个构造函数(除了文字)都有一个Typ
给出关联表达式类型的字段。 (实际上,我们可以添加一个Typ
字段也包含在文字中,即使它是多余的。)如果有一个辅助函数从ExprT
:
exprTyp :: ExprT -> Typ
exprTyp (BoolLit _) = BoolT
exprTyp (NumLit _) = NumT
exprTyp (VarT t _) = t
exprTyp (UnT t _ _) = t
exprTyp (BinT t _ _ _) = t
exprTyp (LetT t _ _ _) = t
类型检查将在一个 monad 中进行,该 monad 跟踪变量的类型(这是我们无法通过检查表达式立即弄清楚的一件事):
type TypContext = [(String, Typ)] -- context of variable types
type TC = ExceptT Error (Reader TypContext)
现在,我们可以只使用字符串来处理类型错误:
type Error = String
我们的类型检查器非常容易编写。我采用非类型化表达式ExprU
,并添加适当的类型注释来制作ExprT
:
tc :: ExprU -> TC ExprT
创建文字的“类型化版本”很容易:
tc (FalseU) = pure $ BoolLit False
tc (TrueU) = pure $ BoolLit True
tc (NumU x) = pure $ NumLit x
在我们的语言中,变量的类型也很简单。我们只允许使用变量after它们已经被定义(由LetU
绑定 - 见下文),因此它们的类型在当前上下文中始终可用:
tc (VarU var) = do
mt <- asks (lookup var)
case mt of
Just t -> pure $ VarT t var
Nothing -> throwError $ "undefined variable " ++ var
一元运算符的类型很简单。仅有的两个一元运算符是“negate”和“not”,并且两者仅适用于精确的一种参数类型并产生精确的一种结果类型。这unTyp
函数在where
子句告诉我们什么UnTyp
我们的一元运算符有:
tc (UnU op e) = do
let UnTyp targ tresult = unTyp op
e' <- tc e
let t = exprTyp e'
when (t /= targ) $ throwError $ "op " ++ show op ++
" expected arg of type " ++ show targ ++ ", got " ++ show t
pure $ UnT tresult op e'
where
unTyp NegOp = UnTyp NumT NumT
unTyp NotOp = UnTyp BoolT BoolT
对于二元运算符,EqualsOp
有点复杂。我们想要实现一些临时多态性,以便它可以应用于布尔值和数字,尽管我们要求类型匹配(所以False == 1
是不允许的)。因此,我们将检查参数的类型并确保它们匹配。然而,无论参数是什么类型,参数的类型BinU EqualsOp _ _
表达式将始终为布尔值,因此键入的版本将始终为BinT BootT EqualsOp _ _
:
tc (BinU EqualsOp e1 e2) = do
e1' <- tc e1
e2' <- tc e2
let t1 = exprTyp e1'
t2 = exprTyp e2'
when (t1 /= t2) $ throwError $ "op EqualOp needs to compare equal types"
pure $ BinT BoolT EqualsOp e1' e2'
其他二元运算符是单类型的,因此我们以与上面的(单类型)一元运算符相同的方式处理它们:
tc (BinU op e1 e2) = do
let BinTyp targ1 targ2 tresult = binTyp op
e1' <- tc e1
e2' <- tc e2
let t1 = exprTyp e1'
t2 = exprTyp e2'
when (t1 /= targ1) $ throwError $ "op " ++ show op ++
" expected left arg of type " ++ show targ1 ++ ", got " ++ show t1
when (t2 /= targ2) $ throwError $ "op " ++ show op ++
" expected right arg of type " ++ show targ2 ++ ", got " ++ show t2
pure $ BinT tresult op e1' e2'
where
binTyp PlusOp = BinTyp NumT NumT NumT
binTyp MulOp = BinTyp NumT NumT NumT
binTyp AndOp = BinTyp BoolT BoolT BoolT
binTyp OrOp = BinTyp BoolT BoolT BoolT
binTyp EqualsOp = error "internal error"
您可能期望进行类型检查LetU
表达式虽然复杂,但是很简单。为了let x=exp1 in exp2
,我们只计算类型exp1
,然后添加该类型x
计算类型时的类型上下文exp2
:
tc (LetU var e1 e2) = do
e1' <- tc e1
let t1 = exprTyp e1'
e2' <- local ((var,t1):) $ tc e2
let t2 = exprTyp e2'
pure $ LetT t2 var e1' e2'
这就是类型检查器的全部内容。
一旦运行类型检查器来创建ExprT
对于声音类型,我们需要对其进行评估。我们将表达式的值表示为:
-- Values
data Value
= BoolV Bool
| NumV Double
deriving (Show)
评估将在具有为变量赋值的上下文的 monad 中进行:
type ValContext = [(String, Value)] -- context of variable values
type E = Reader ValContext
请注意,我们不需要ExceptT
变压器在这里。事实证明,经过类型检查的程序无法在我们的语言中生成运行时错误。
评估员/口译员:
eval :: ExprT -> E Value
以明显的方式评估文字:
eval (BoolLit b) = pure $ BoolV b
eval (NumLit x) = pure $ NumV x
在当前上下文中查找变量值:
eval (VarT _ var) = do
mt <- asks (lookup var)
case mt of
Just v -> pure $ v
Nothing -> internalerror
请注意,类型检查器已经确保变量仅在定义时才使用,因此“不会发生”失败的查找。我们使用该函数internalerror
(定义如下)以使编译器确信所有情况都已处理,这样我们就可以避免警告,但是internalerror
除非我们的解释器中有错误,否则不会被调用。
一元运算符的解释如下:
eval (UnT _ op e) = run op <$> eval e
where run NegOp (NumV x) = NumV (-x)
run NotOp (BoolV b) = BoolV (not b)
run _ _ = internalerror
同样,由于类型检查器的原因,我们无法应用NegOp
为布尔值或NotOp
到一个数字,所以这里缺少的案例(例如,run NegOp (BoolV b)
)不可能发生。不幸的是,这意味着我们失去了一些好处-Wall
打开——如果我们忘记处理一个新的操作符,它会抛出一个internalerror
在运行时。
二元运算符的解释类似:
eval (BinT _ op e1 e2) = run op <$> eval e1 <*> eval e2
where run EqualsOp (BoolV v1) (BoolV v2) = BoolV $ v1 == v2
run EqualsOp (NumV v1) (NumV v2) = BoolV $ v1 == v2
run PlusOp (NumV v1) (NumV v2) = NumV $ v1 + v2
run MulOp (NumV v1) (NumV v2) = NumV $ v1 * v2
run AndOp (BoolV v1) (BoolV v2) = BoolV $ v1 && v2
run OrOp (BoolV v1) (BoolV v2) = BoolV $ v1 || v2
run _ _ _ = internalerror
因为哈斯克尔==
运算符是多态的,我们可以添加一个Eq
实例到Value
输入并将前两种情况替换为:
where run EqualsOp v1 v2 = BoolV $ v1 == v2
但我想说明一个事实EqualsOp (BoolV v1) (NumV v2)
在经过类型检查的程序中永远不会发生。
最后,我们处理LetT
像这样:
eval (LetT _ var e1 e2) = do
v1 <- eval e1
local ((var,v1):) $ eval e2
那是我们的评估员/口译员。
一些有趣的事情需要注意。在我们的定义中eval
,我们从未真正引用过类型注释tc
已添加!出于这个原因,我们实际上可以写eval
解释原始的无类型ExprU
因为事实上tc
完成就足以确保ExprU
可以在没有运行时类型错误的情况下进行解释。也就是说,用这种简单的语言来说,fact程序类型检查比类型检查期间计算的类型更重要。在更复杂的语言中,类型注释可能更有用。
无论如何,就是这样。这是完整的代码和示例程序expr1
用目标语言:
{-# OPTIONS_GHC -Wall #-}
import Control.Monad
import Control.Monad.Reader
import Control.Monad.Except
-- Untyped expressions
data ExprU
= FalseU | TrueU -- boolean literals
| NumU Double -- numeric literal
| VarU String -- variable
| UnU UnOp ExprU -- unary operator
| BinU BinOp ExprU ExprU -- binary operator
| LetU String ExprU ExprU -- let x = expr1 in expr2
data UnOp = NegOp | NotOp
deriving (Show)
data BinOp = PlusOp | MulOp | AndOp | OrOp | EqualsOp
deriving (Show)
-- Simple expression types
data Typ
= BoolT
| NumT
deriving (Show, Eq)
-- Types of operators
data BinTyp = BinTyp Typ Typ Typ
data UnTyp = UnTyp Typ Typ
-- Typed expressions
data ExprT
= BoolLit Bool
| NumLit Double
| VarT Typ String
| UnT Typ UnOp ExprT
| BinT Typ BinOp ExprT ExprT
| LetT Typ String ExprT ExprT
exprTyp :: ExprT -> Typ
exprTyp (BoolLit _) = BoolT
exprTyp (NumLit _) = NumT
exprTyp (VarT t _) = t
exprTyp (UnT t _ _) = t
exprTyp (BinT t _ _ _) = t
exprTyp (LetT t _ _ _) = t
-- Type check an expression
type Error = String
type TypContext = [(String, Typ)] -- context of variable types
type TC = ExceptT Error (Reader TypContext)
runTC :: TC a -> a
runTC act = case runReader (runExceptT act) [] of
Left err -> error err
Right a -> a
tc :: ExprU -> TC ExprT
tc (FalseU) = pure $ BoolLit False
tc (TrueU) = pure $ BoolLit True
tc (NumU x) = pure $ NumLit x
tc (VarU var) = do
mt <- asks (lookup var)
case mt of
Just t -> pure $ VarT t var
Nothing -> throwError $ "undefined variable " ++ var
tc (UnU op e) = do
let UnTyp targ tresult = unTyp op
e' <- tc e
let t = exprTyp e'
when (t /= targ) $ throwError $ "op " ++ show op ++
" expected arg of type " ++ show targ ++ ", got " ++ show t
pure $ UnT tresult op e'
where
unTyp NegOp = UnTyp NumT NumT
unTyp NotOp = UnTyp BoolT BoolT
tc (BinU EqualsOp e1 e2) = do
e1' <- tc e1
e2' <- tc e2
let t1 = exprTyp e1'
t2 = exprTyp e2'
when (t1 /= t2) $ throwError $ "op EqualOp needs to compare equal types"
pure $ BinT BoolT EqualsOp e1' e2'
tc (BinU op e1 e2) = do
let BinTyp targ1 targ2 tresult = binTyp op
e1' <- tc e1
e2' <- tc e2
let t1 = exprTyp e1'
t2 = exprTyp e2'
when (t1 /= targ1) $ throwError $ "op " ++ show op ++
" expected left arg of type " ++ show targ1 ++ ", got " ++ show t1
when (t2 /= targ2) $ throwError $ "op " ++ show op ++
" expected right arg of type " ++ show targ2 ++ ", got " ++ show t2
pure $ BinT tresult op e1' e2'
where
binTyp PlusOp = BinTyp NumT NumT NumT
binTyp MulOp = BinTyp NumT NumT NumT
binTyp AndOp = BinTyp BoolT BoolT BoolT
binTyp OrOp = BinTyp BoolT BoolT BoolT
binTyp EqualsOp = error "internal error"
tc (LetU var e1 e2) = do
e1' <- tc e1
let t1 = exprTyp e1'
e2' <- local ((var,t1):) $ tc e2
let t2 = exprTyp e2'
pure $ LetT t2 var e1' e2'
-- Evaluate a typed expression
internalerror :: a
internalerror = error "can't happen, internal error in type checker"
-- Values
data Value
= BoolV Bool
| NumV Double
deriving (Show)
type ValContext = [(String, Value)] -- context of variable values
type E = Reader ValContext
runE :: E a -> a
runE act = runReader act []
eval :: ExprT -> E Value
eval (BoolLit b) = pure $ BoolV b
eval (NumLit x) = pure $ NumV x
eval (VarT _ var) = do
mt <- asks (lookup var)
case mt of
Just v -> pure $ v
Nothing -> internalerror
eval (UnT _ op e) = run op <$> eval e
where run NegOp (NumV x) = NumV (-x)
run NotOp (BoolV b) = BoolV (not b)
run _ _ = internalerror
eval (BinT _ op e1 e2) = run op <$> eval e1 <*> eval e2
where run EqualsOp (BoolV v1) (BoolV v2) = BoolV $ v1 == v2
run EqualsOp (NumV v1) (NumV v2) = BoolV $ v1 == v2
run PlusOp (NumV v1) (NumV v2) = NumV $ v1 + v2
run MulOp (NumV v1) (NumV v2) = NumV $ v1 * v2
run AndOp (BoolV v1) (BoolV v2) = BoolV $ v1 && v2
run OrOp (BoolV v1) (BoolV v2) = BoolV $ v1 || v2
run _ _ _ = internalerror
eval (LetT _ var e1 e2) = do
v1 <- eval e1
local ((var,v1):) $ eval e2
expr1 :: ExprU
expr1 = LetU "x" (BinU PlusOp (NumU 2) (NumU 3)) (LetU "y" (BinU MulOp (VarU "x") (NumU 5)) (BinU EqualsOp (VarU "y") (NumU 25)))
val1 :: Value
val1 = let e1' = runTC (tc expr1) in runE (eval e1')
main :: IO ()
main = do
print $ val1