我想在SML中实现任意签名。如何为该签名上的术语定义数据类型?我需要它来编写检查术语是否格式正确的函数。
答案 0 :(得分:1)
在我看来,有两种主要的方式来表示AST。既可以是一系列(可能是相互递归的)数据类型,也可以是一个大数据类型。两者都有优点。
如果我们定义以下BNF(从SML定义中提取并略微简化)
<exp> ::= <exp> andalso <exp>
| <exp> orelse <exp>
| raise <exp>
| <appexp>
<appexp> ::= <atexp> | <appexp> <atexp>
<atexp> ::= ( <exp>, ..., <exp> )
| [ <exp>, ..., <exp> ]
| ( <exp> ; ... ; <exp> )
| ( <exp> )
| ()
如上所述,这是简化的,大部分的atexp被遗漏了。
在这里,您可以为表达式,声明,模式等创建数据类型。 基本上,您将为BNF中的每个非终端创建数据类型。
我们很可能会创建以下数据类型
datatype exp = Andalso of exp * exp
| Orelse of exp * exp
| Raise of exp
| App of exp * atexp
| Atexp of atexp
and atexp = Tuple of exp list
| List of exp list
| Seq of exp list
| Par of exp
| Unit
请注意,非终端已被消耗为exp数据类型,而不是将其作为自己的终端。那会毫无理由地混淆AST。你必须记住,BNF通常以这样的方式编写,即它也定义了先行和协同(例如,算术)。在这种情况下,您通常可以通过将多个非终端合并为一种数据类型来简化BNF。
定义多个数据类型的好处是你可以获得一些很好的AST。例如,如果我们也有非终端声明,我们知道AST将更新在列表中包含一个声明(因为只有表达式可以在那里)。因此,大多数成形性检查都不是必需的。
然而,这并不总是一件好事。通常你需要对AST进行一些检查,例如类型检查。在许多情况下,BNF非常大,因此对AST建模的数据类型的数量也非常大。牢记这一点,您必须为每种数据类型创建一个函数,对于您不希望在AST上进行的每种修改。在许多情况下,您只想更改AST的一小部分,但您(很可能)仍需要为每种数据类型定义一个函数。这些功能中的大多数基本上都是身份,然后只有少数情况下你会做所需的工作。
例如,如果我们不计算给定AST中有多少单位,我们可以定义以下函数
fun countUnitexp (Andalso (e1, e2)) = countUnitexp e1 + countUnitexp e2
| countUnitexp (Orelse (e1, e2)) = countUnitexp e1 + countUnitexp e2
| countUnitexp (Raise e1) = countUnitexp e1
| countUnitexp (App (e1, atexp)) = countUnitexp e1 + countUnitatexp atexp
| countUnitexp (Atexp atexp) = countUnitatexp atexp
and countUnitatexp (Tuple exps) = sumUnit exps
| countUnitatexp (List exps) = sumUnit exps
| countUnitatexp (Seq exps) = sumUnit exps
| countUnitatexp (Par exp) = countUnitexp exp
| countUnitatexp Unit = 1
and sumUnit exps = foldl (fn (exp,b) => b + countUnitexp exp) 0 exps
正如您可能已经看到我们正在做很多工作,只是为了这个简单的任务。想象一下更大的语法和更复杂的任务。
让我们结合之前的数据类型,但改变它们,使它们(他们自己)不包含他们的孩子。因为在这种方法中我们构建了一个树结构,它有一个节点和该节点的一些子节点。显然,如果你有一个标识符,那么标识符需要包含实际的字符串表示(例如,变量名)。
因此,我们首先定义树结构的节点。
(* The comment is the kind of children and possibly specific number of children
that the BNF defines to be valid *)
datatype node = Exp_Andalso (* [exp, exp] *)
| Exp_Orelse (* [exp, exp] *)
| Exp_Raise (* [exp] *)
| Exp_App (* [exp, atexp] *)
(* Superflous:| Exp_Atexp (* [atexp] *) *)
| Atexp_Tuple (* exp list *)
| Atexp_List (* exp list *)
| Atexp_Seq (* exp list *)
| Atexp_Par (* [exp] *)
| Atexp_Unit (* [] *)
看看来自tupe的Atexp如何变得超级丰富,因此我们将其删除。就个人而言,我认为通过告诉我们可以期待哪些孩子(在树结构中)来接下来发表评论是很好的。
(* Note this is a non empty tree. That is you have to pack it in an option type
if you wan't to represent an empty tree *)
datatype 'a tree = T of 'a * 'a tree list
(* Define the ast as trees of our node datatype *)
type ast = node tree
然后,我们定义一个通用树,并将类型ast定义为“节点树”。 如果您使用某个库,那么很可能已经存在这样的树结构。此外,将这个树结构扩展为不仅仅包含节点作为数据也可能很方便,但是我们只是在这里保持简单。
fun foldTree f b (T (n, [])) = f (n, b)
| foldTree f b (T (n, ts)) = foldl (fn (t, b') => foldTree f b' t)
(f (n, b)) ts
对于这个例子,我们在树上定义了一个折叠函数,如果你使用的是库,那么折叠,映射等所有这些函数很可能已经被定义了。
fun countUnit (Atexp_Unit) = 1
| countUnit _ = 0
如果我们从之前的例子开始,我们不计算单位的出现次数,那么我们就可以在树上折叠上述函数。
val someAST = T(Atexp_Tuple, [ T (Atexp_Unit, [])
, T (Exp_Raise, [])
, T (Atexp_Unit, [])
]
)
一个简单的AST看起来像上面那样(请注意,这实际上是无效的,因为我们有一个没有子节点的Exp_Raise)。然后我们可以通过
进行计数foldTree (fn (a,b) => (countUnit a) + b) 0 someAST
这种方法的缺点是你必须编写一个检查函数来验证你的AST是否格式正确,因为创建AST时没有任何限制。这包括孩子们具有正确的“类型”(例如,只有Exp_ *作为Exp_Andalso中的孩子)并且有正确数量的孩子(例如,Exp_Andalso中恰好有两个孩子)。
这种方法还需要一些构建开始,因为你不使用一些已定义树的库(包括用于修改树的辅助函数)。但从长远来看,这是值得的。