我想在 SML 中实现任意签名。如何为该签名上的术语定义数据类型?我需要它来编写检查术语是否格式正确的函数。
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 都被省略了。
1.一系列可能相互递归的数据类型
例如,您可以在这里为表达式、声明、模式等创建一个数据类型。基本上,您将为 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
如您所见,我们正在做很多工作,只是为了这个简单的任务。想象一个更大的语法和更复杂的任务。
2. 一个(大)数据类型(节点)——以及这些节点的树
让我们组合之前的数据类型,但更改它们以使它们(它们自己)不包含它们的子对象。因为在这种方法中,我们构建了一个树结构,该结构具有一个节点和该节点的一些子节点。显然,如果您有一个标识符,那么该标识符需要包含实际的字符串表示形式(例如,变量名)。
因此,让我们从定义树结构的节点开始。
(* 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 中恰好有两个孩子)。
鉴于您不使用某些定义了树的库(包括用于修改树的辅助函数),这种方法还需要一些构建入门。然而,从长远来看,这是值得的。