选择是否使用歧视联盟或记录类型在F#中的小型AST#
假设我正在实现一个非常简单的玩具语言解析器。我正在决定是否使用DU或记录类型(可能是两者的混合?)。语言的结构将是:在这个简单的语言编写的程序的选择是否使用歧视联盟或记录类型在F#中的小型AST#
a Namespace consists of a name and a list of classes
a Class consists of a name and a list of methods
Method consists of a name, return type and a list of Arguments
Argument consists of a type and a name
例子:
namespace ns {
class cls1 {
void m1() {}
}
class cls2 {
void m2(int i, string j) {}
}
}
你将如何对此建模,为什么?
您几乎肯定希望使用DU来实现更改,其中代码结构的任何部分都可能是多种可能性之一。尽管你可以使用元组代替记录 - 这可能会使它更易于使用,但可能更难以阅读和维护,因为在元组中没有命名项,所以混合可能是理想的。
我将其建模为这样的事情
type CompilationUnit = | Namespace list
and Namespace = { Name : String
Body : NamespaceBody }
and NamespaceBody = | Classes of Class list
and Class = { Name : String
Body : ClassBody }
and ClassBody = | Members of Member list
and Member = | Method of Method
and Method = { Name : String
Parameters : Parameter list option
ReturnType : TypeName option
Body : MethodBody }
and Parameter = { Name : String
Type : TypeName }
and MethodBody = ...
and TypeName = ...
使用例如语言的需要下游用户可能并不明显,但你必须在代码中任何一点可能是一个或将尽快明朗化更多的项目。比如说,如果你为你的班级添加字段 - 你只需要添加一个新的Field
歧视Member
。
如果您使用语法来分析你的语言(LL/LALR或类似的),你可能需要对你的语法各有交替规则匹配DU。
一个命名空间包含名称和类 一类由名称的列表和方法列表 方法由名称,返回类型和参数列表的 参数由一个类型的和在这个简单的语言编写的程序的名称 例子:
您还需要一个类型定义为你的类型系统,实际上就是联合类型是宝贵的唯一的地方:
type Type = Void | Int | String
所以你的语言中的一个类型是一个int或一个字符串或void,但不能为空(例如, null)并且不能超过其中一个选项。
类型命名空间的可能是完全匿名的,就像这样:
string * (string * (Type * string * (Type * string) list) list) list
你可以定义你的榜样命名空间是这样的:
"ns", ["cls1", [Void, "m1", []]
"cls2", [Void, "m2", [Int, "i"; String, "j"]]]
在实践中,你可能想要把能力在其他命名空间中的命名空间,并将类放在类中,以便您可以将代码演变为如下形式:
type Type =
| Void
| Int
| String
| Class of Map<string, Type> * Map<string, Type * (Type * string) list>
type Namespace =
| Namespace of string * Namespace list * Map<string, Type>
Namespace("ns", [],
Map
[ "cls1", Class(Map[], Map["m1", (Void, [])])
"cls2", Class(Map[], Map["m2", (Void, [Int, "i"; String, "j"])])])
匿名类型都很好,只要它们不会引起混淆。作为一个经验法则,如果你有两个或三个字段,他们是不同类型的(如“方法”在这里),然后一个元组是好的。如果有更多的字段或具有相同类型的多个字段,那么是时候切换到记录类型。
因此,在这种情况下,你可能想引入一个记录类型的方法:
type Method =
{ ReturnType: Type
Arguments: (Type * string) list }
and Type =
| Void
| Int
| String
| Class of Map<string, Type> * Map<string, Method>
type Namespace =
| Namespace of string * Namespace list * Map<string, Type>
Namespace("ns", [],
Map
[ "cls1", Class(Map[], Map["m1", { ReturnType = Void; Arguments = [] }])
"cls2", Class(Map[], Map["m2", { ReturnType = Void; Arguments = [Int, "i"; String, "j"] }])])
,也许一个辅助功能来构建这些记录:
let Method retTy name args =
name, { ReturnType = retTy; Arguments = args }
Namespace("ns", [],
Map
[ "cls1", Class(Map[], Map[Method Void "m1" []])
"cls2", Class(Map[], Map[Method Void "m2" [Int, "i"; String, "j"]])])
嗯,所以两者的混合,我有点期待.. –
我倾向于用元组来使用DU。它们更容易编写,但可能会让人困惑,因为你不仅仅有几个参数,而且你依赖文档来告诉你每个参数的用途。例如,对于Method中的Parameters,您可以改为说'Parameters:(String * TypeName)列表选项',这显然是每个元组表示的项目。但是,如果您也是使用String作为类型,而不是TypeName,并且使用了'(String * String)',那么需要记录来理解每个对象的用途。 –
我通常会尝试在可读性方面犯错,所以我想我会坚持记录而不是元组。 –