SML中签名上的术语的数据类型

时间:2011-03-14 10:41:16

标签: smlnj

我想在SML中实现任意签名。如何为该签名上的术语定义数据类型?我需要它来编写检查术语是否格式正确的函数。

1 个答案:

答案 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被遗漏了。

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中恰好有两个孩子)。

这种方法还需要一些构建开始,因为你不使用一些已定义树的库(包括用于修改树的辅助函数)。但从长远来看,这是值得的。