我在F#中定义了一个表达式树结构,如下所示:
type Num = int
type Name = string
type Expr =
| Con of Num
| Var of Name
| Add of Expr * Expr
| Sub of Expr * Expr
| Mult of Expr * Expr
| Div of Expr * Expr
| Pow of Expr * Expr
| Neg of Expr
我想能够漂亮地打印表达式树,所以我做了以下操作:
let (|Unary|Binary|Terminal|) expr =
match expr with
| Add(x, y) -> Binary(x, y)
| Sub(x, y) -> Binary(x, y)
| Mult(x, y) -> Binary(x, y)
| Div(x, y) -> Binary(x, y)
| Pow(x, y) -> Binary(x, y)
| Neg(x) -> Unary(x)
| Con(x) -> Terminal(box x)
| Var(x) -> Terminal(box x)
let operator expr =
match expr with
| Add(_) -> "+"
| Sub(_) | Neg(_) -> "-"
| Mult(_) -> "*"
| Div(_) -> "/"
| Pow(_) -> "**"
| _ -> failwith "There is no operator for the given expression."
let rec format expr =
match expr with
| Unary(x) -> sprintf "%s(%s)" (operator expr) (format x)
| Binary(x, y) -> sprintf "(%s %s %s)" (format x) (operator expr) (format y)
| Terminal(x) -> string x
但是,我并不喜欢operator
函数的failwith
方法,因为它在编译时不安全。所以我把它改写为一个活动模式:
let (|Operator|_|) expr =
match expr with
| Add(_) -> Some "+"
| Sub(_) | Neg(_) -> Some "-"
| Mult(_) -> Some "*"
| Div(_) -> Some "/"
| Pow(_) -> Some "**"
| _ -> None
现在我可以漂亮地重写我的format
函数如下:
let rec format expr =
match expr with
| Unary(x) & Operator(op) -> sprintf "%s(%s)" op (format x)
| Binary(x, y) & Operator(op) -> sprintf "(%s %s %s)" (format x) op (format y)
| Terminal(x) -> string x
我认为,既然F#是魔法,这就行了。不幸的是,编译器随后警告我模式匹配不完整,因为它看不到任何与Unary(x)
匹配的东西也将与Operator(op)
匹配,而所有与Binary(x, y)
匹配的东西都将与Operator(op)
匹配。我认为这样的警告和编译器错误一样糟糕。
所以我的问题是:这不起作用有什么具体的原因吗?有没有一个简单的解决方法可以让我获得我想要的安全类型?这种类型的编译时检查是否存在固有问题,或者F#可能会在未来的版本中添加一些问题?
如果将基础术语和复杂术语之间的目的地编码到类型系统中,则可以避免运行时检查,并使它们成为完全的模式匹配。
type Num = int
type Name = string
type GroundTerm =
| Con of Num
| Var of Name
type ComplexTerm =
| Add of Term * Term
| Sub of Term * Term
| Mult of Term * Term
| Div of Term * Term
| Pow of Term * Term
| Neg of Term
and Term =
| GroundTerm of GroundTerm
| ComplexTerm of ComplexTerm
let (|Operator|) ct =
match ct with
| Add(_) -> "+"
| Sub(_) | Neg(_) -> "-"
| Mult(_) -> "*"
| Div(_) -> "/"
| Pow(_) -> "**"
let (|Unary|Binary|) ct =
match ct with
| Add(x, y) -> Binary(x, y)
| Sub(x, y) -> Binary(x, y)
| Mult(x, y) -> Binary(x, y)
| Div(x, y) -> Binary(x, y)
| Pow(x, y) -> Binary(x, y)
| Neg(x) -> Unary(x)
let (|Terminal|) gt =
match gt with
| Con x -> Terminal(string x)
| Var x -> Terminal(string x)
let rec format expr =
match expr with
| ComplexTerm ct ->
match ct with
| Unary(x) & Operator(op) -> sprintf "%s(%s)" op (format x)
| Binary(x, y) & Operator(op) -> sprintf "(%s %s %s)" (format x) op (format y)
| GroundTerm gt ->
match gt with
| Terminal(x) -> x
此外,imo,如果你想确保类型安全,你应该避免拳击。如果你真的想要两种情况,做两种模式。或者,正如这里所做的那样,只需对稍后需要的类型进行投影。这样可以避免装箱,而是返回打印所需的内容。
我认为您可以使operator
成为一个正常函数,而不是一个活动模式。因为运算符只是一个为expr
提供运算符字符串的函数,其中unary
、binary
和terminal
是表达式类型,因此对它们进行模式匹配是有意义的。
let operator expr =
match expr with
| Add(_) -> "+"
| Sub(_) | Neg(_) -> "-"
| Mult(_) -> "*"
| Div(_) -> "/"
| Pow(_) -> "**"
| Var(_) | Con(_) -> ""
let rec format expr =
match expr with
| Unary(x) -> sprintf "%s(%s)" (operator expr) (format x)
| Binary(x, y) -> sprintf "(%s %s %s)" (format x) (operator expr) (format y)
| Terminal(x) -> string x
我发现最好的解决方案是重组您原来的类型定义:
type UnOp = Neg
type BinOp = Add | Sub | Mul | Div | Pow
type Expr =
| Int of int
| UnOp of UnOp * Expr
| BinOp of BinOp * Expr * Expr
然后可以在UnOp
和BinOp
类型上编写各种函数,包括选择运算符。将来,您甚至可能希望将BinOp
拆分为算术运算符和比较运算符。
例如,我在(非免费)文章"面向语言的编程:术语级解释器"中使用了这种方法"(2008)发表在F#期刊上。