如何在Haskell中定义各类语言构造的抽象语法?
用Haskell定义目标语言的抽象语法(AST)
一、先理清核心逻辑
你要做的不是描述Haskell自身的语法,而是用Haskell的数据类型,把你设计的目标语言的各种语法结构(变量声明、表达式等)抽象成语法树(AST)。简单说,就是把目标语言里每一种可写的代码结构,对应成Haskell里的代数数据类型(ADT)构造子。
二、变量声明的正确写法
你之前写的var = int | bool | String是混淆了变量声明的结构和值的类型,完全搞错了方向。目标语言里的变量声明是「变量名 + 可选初始值」,初始值只能是三种字面量,所以先定义目标语言的字面量类型:
-- 目标语言支持的字面量 data Literal = IntLit Int | BoolLit Bool | StringLit String deriving (Show, Eq)
再基于这个定义变量声明的结构:
-- 目标语言的变量声明 data VarDecl = VarDecl String (Maybe Literal) deriving (Show, Eq)
举个例子:
var x = 10;对应VarDecl "x" (Just (IntLit 10))var y;对应VarDecl "y" Nothing
三、算术与原始表达式的定义
原始表达式要覆盖你列出的所有场景,同样用代数数据类型枚举所有可能的表达式类型:
-- 目标语言的表达式类型 data Expr = LitExpr Literal -- 字面量表达式(比如10、true、"abc") | VarExpr String -- 变量引用(比如x) | MemberExpr Expr String -- 类成员访问(比如obj.name) | ArrayIndex Expr Expr -- 数组索引(比如arr[2]) | FuncCall Expr [Expr] -- 函数调用(比如foo(1, false)) | BinOp Expr Op Expr -- 算术/逻辑二元操作(比如a + b、x == y) deriving (Show, Eq) -- 支持的运算符 data Op = Add | Sub | Mul | Div | Eq | Neq | And | Or deriving (Show, Eq)
每个构造子的对应场景:
LitExpr:把字面量包装成表达式(因为字面量本身就是一种表达式)VarExpr:直接引用变量名MemberExpr:第一个参数是对象的表达式,第二个是成员名称ArrayIndex:第一个参数是数组的表达式,第二个是索引的表达式FuncCall:第一个参数是函数的表达式(可以是变量名或其他表达式),第二个是参数列表
四、实用理解技巧
- 拆分语法单元:把目标语言的每个语法结构拆成最小组成部分,比如变量声明拆成「变量名」和「可选初始值」,表达式拆成「字面量」「变量引用」等子类型,每个子类型对应ADT的一个构造子。
- 区分元语言和目标语言:Haskell是用来描述目标语言的「元语言」,别把两者的语法混了——比如Haskell的
Int是元语言的类型,对应目标语言里的IntLit构造子。 - 从具体代码反推:先写几个目标语言的代码示例,再思考怎么用Haskell的数据类型把它们表示出来,比如先想
var x = "hello";对应什么Haskell值,再反推VarDecl的定义。
五、关于提问场景
这类编译器构造、语法抽象的问题在这里提问完全合适。如果需要更聚焦的讨论,国内可以参与知乎的编译器相关话题、编程社区论坛;国外则可以在Stack Overflow的haskell或compiler-construction标签下交流。
内容的提问来源于stack exchange,提问作者Magvy
相关产品推荐
相关产品推荐

