原文:http://2ality.com/2017/12/variants-reasonml.html
翻译:ppp
系列文章目录详见: “什么是ReasonML?”
可变类型(简称:变型)是许多函数式编程语言支持的数据类型。 它们是ReasonML中的一个重要组成部分,但C语言(C,C ++,Java,C#等)并不支持。 本文将解释了它们是如何工作的。
可变类型允许自定义符号集。这样的用法与C语言中的枚举类似。例如,以下类型color定义了六种颜色的符号。
type color = Red | Orange | Yellow | Green | Blue | Purple;
这个类型定义中有两个要点: 类型的名称color,必须以小写字母开头。 构造函数的名称(Red,Orange...)必须以大写字母开头。当我们把可变类型当做数据结构使用时,为什么称之为构造函数就更容易理解了。 构造函数的名称在当前范围内必须是唯一的。这是为了让ReasonML能够轻松推导出它们的类型:
# Purple;
- : color = Purple
可变类型可以通过switch和模式匹配来进行处理:
let invert = (c: color) =>
switch c {
| Red => Green
| Orange => Blue
| Yellow => Purple
| Green => Red
| Blue => Orange
| Purple => Yellow
};
这里,构造函数既用作模式(=>的左边),也用于数值(=>的右边)。这是invert()的实际使用:
# invert(Red);
- : color = Green
# invert(Yellow);
- : color = Purple
在ReasonML中,可变类型通常是比布尔变量更好的选择。举个例子,定义函数:(请记住,在ReasonML中,主要参数最终都会进行柯里化currying)
let stringOfContact(includeDetails: bool, c: contact) => ···;
这是stringOfContact的调用:
let str = stringOfContact(true, myContact);
现在还是不清楚这个布尔值有什么作用。你可以通过参数别名来做一些改进。
let stringOfContact(~includeDetails: bool, c: contact) => ···;
let str = stringOfContact(~includeDetails=true, myContact);
那么更具可读性的写法是为值引入一个可变类型~includeDetails:
type includeDetails = ShowEverything | HideDetails;
let stringOfContact(~levelOfDetail: includeDetails, c: contact) => ···;
let str = stringOfContact(~levelOfDetail=ShowEverything, myContact);
使用可变类型includeDetails有两个好处: 立即知道“不显示细节”的含义。 以后添加模块变得更容易。
有时,你想使用枚举值作为数据的索引。可以通过将枚举值映射到数据的函数来实现:
type color = Red | Orange | Yellow | Green | Blue | Purple;
let stringOfColor(c: color) =>
switch c {
| Red => "Red"
| Orange => "Orange"
| Yellow => "Yellow"
| Green => "Green"
| Blue => "Blue"
| Purple => "Purple"
};
这样实现有一个缺点:它会导致代码的冗余,特别是如果你想要将多个数据片段与相同的变量值相关联时。我们将在未来的文章中探索备选方案。
每个构造函数都可以接收一个或多个值。这些值由位置标识。也就是说,个别的构造函数与元组相似。以下代码对这点做出了说明。
type point = Point(float, float);
type shape =
| Rectangle(point, point)
| Circle(point, float);
point类型是具有一个构造函数的可变类型。它拥有两个浮点数。shape是另一种可变类型。它可以表示: Rectangle由两个角坐标确定的矩形或, Circle由一个中心和半径确定的圆。 当构造函数有多个参数时,如果参数没有别名,将会遇到一个问题 - 我们必须在别处描述它们的含义。在这种情况下,我们可以使用Records(将在未来的章中进行说明)。 下面是如何使用构造函数:
# let bottomLeft = Point(-1.0, -2.0);
let bottomLeft: point = Point(-1., -2.);
# let topRight = Point(7.0, 6.0);
let topRight: point = Point(7., 6.);
# let circ = Circle(topRight, 5.0);
let circ: shape = Circle(Point(7., 6.), 5.);
# let rect = Rectangle(bottomLeft, topRight);
let rect: shape = Rectangle(Point(-1., -2.), Point(7., 6.));
由于每个构造函数名称都是唯一的,所以ReasonML可以轻松推断出这些类型。 如果要在构造函数中保存数据,则通过switch更方便,因为它还允许你访问该数据:
let pi = 4.0 *. atan(1.0);
let computeArea = (s: shape) =>
switch s {
| Rectangle(Point(x1, y1), Point(x2, y2)) =>
let width = abs_float(x2 -. x1);
let height = abs_float(y2 -. y1);
width *. height;
| Circle(_, radius) => pi *. (radius ** 2.0)
};
让我们用一下computeArea继续使用我们之前的rtop会话:
# computeArea(circ);
- : float = 78.5398163397448315
# computeArea(rect);
- : float = 64.
你也可以通过可变类型定义递归数据结构。例如,节点包含整数的二叉树:
type intTree =
| Empty
| Node(int, intTree, intTree);
intTree 值是这样构造的:
let myIntTree = Node(1,
Node(2, Empty, Empty),
Node(3,
Node(4, Empty, Empty),
Empty
)
);
myIntTree看起来如下:1有两个子节点2和3. 2有两个空子节点。等等。
1
2
X
X
3
4
X
X
X
为了演示处理自递归数据结构,让我们实现函数computeSum,它计算存储在节点中的整数的总和。
let rec computeSum = (t: intTree) =>
switch t {
| Empty => 0
| Node(i, leftTree, rightTree) =>
i + computeSum(leftTree) + computeSum(rightTree)
};
computeSum(myIntTree); /* 10 */
这种递归是可变类型类型的典型用法: 一组有限的构造函数用于创建数据。本例中:Empty和Node()。 使用相同的构造函数作为模式来处理数据。 只要它是类型的intTree,这就确保了我们能够正确处理传递给我们的任何数据。如果switch对intTree覆盖不完整,ReasonML会出现警告。这可以避免出现遗漏的情况。为了演示,让我们假设我们漏了Empty,omputeSum定义为:
let rec computeSum = (t: intTree) =>
switch t {
/* Missing: Empty */
| Node(i, leftTree, rightTree) =>
i + computeSum(leftTree) + computeSum(rightTree)
};
然后我们得到以下警告。
Warning: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
Empty
正如在函数那篇文章中所说,如果使用默认分支catch-all(default)意味着你不会得到这样的提示。这就是为什么你应该竟可能的避免这种情况。
回想一下,在涉及到递归的定义是时我们必须使用let rec: 通过let rec完成单一的自递归定义。 多个互递归的定义是通过let rec和and共同完成。 type是隐含的rec。这使我们能够完成自递归的定义,如intTree。对于互递归的定义,我们还需要配合and完成。下面的例子重新定义了intTree,但这次使用了一个单独的节点类型。
type intTree =
| Empty
| IntTreeNode(intNode)
and intNode =
| IntNode(int, intTree, intTree);
intTree和intNode是互递归的,这就是为什么它们需要在相同的type声明中定义,再通过and连接。
让我们回顾一下对intTree的原始定义:
type intTree =
| Empty
| Node(int, intTree, intTree);
我们如何将这个定义转化为树的一个通用定义,树的节点可以包含任何类型的值? 为此,我们必须为某个Node内容的类型引入一个变量。类型变量在ReasonML中以撇号作为前缀。例如:'a。因此,通用树的定义如下所示:
type tree('a) =
| Empty
| Node('a, tree('a), tree('a));
有两件事值得注意。首先,先前为int类型的节点变成'a类型。其次,类型变量'a变成了tree的参数。节点将该参数传递给其子节点。也就是说,我们可以为每棵树选择不同的节点值类型,但在同一棵树中,所有节点必须是相同的类型。 现在我们可以通过传入不同类型的参数tree来定义对应类型树的别名了:
type intTree = tree(int);
我们用tree来创建一个字符串树:
let myStrTree = Node("a",
Node("b", Empty, Empty),
Node("c",
Node("d", Empty, Empty),
Empty
)
);
基于类型推测机制,你不需要提供具体的类型。 ReasonML会自动推断myStrTree具有tree(string)类型。下面的通用函数可以打印任何类型的树:
/**
* @param ~indent How much to indent the current (sub)tree.
* @param ~stringOfValue Converts node values to strings.
* @param t The tree to convert to a string.
*/
let rec stringOfTree = (~indent=0, ~stringOfValue: 'a => string, t: tree('a)) => {
let indentStr = String.make(indent*2, ' ');
switch t {
| Empty => indentStr ++ "X" ++ "\n"
| Node(x, leftTree, rightTree) =>
indentStr ++ stringOfValue(x) ++ "\n" ++
stringOfTree(~indent=indent+1, ~stringOfValue, leftTree) ++
stringOfTree(~indent=indent+1, ~stringOfValue, rightTree)
};
};
该函数以递归的方式遍历t的所有节点。鉴于stringOfTree可以使用任意类型'a,我们需要一个类型确定函数来将'a类型的值转换为字符串。这就是~stringOfValue的作用。 这是我们如何打印我们先前定义的myStrTree:
# print_string(stringOfTree(~stringOfValue=x=>x, myStrTree));
a
b
X
X
c
d
X
X
X
我将简要介绍两种常用的标准可变类型。未来的文章将给出使用它们的例子。
在许多面向对象的语言中,具有类型string的变量意味着该变量可以是null,也可以是字符串值。包含null的类型null称为可空类型。可空类型会有个问题就是,如果忘记处理它们的值很容易为null。如果意外的出现null ,将抛出臭名昭着的空指针异常。 在ReasonML中,类型不可空。相反,可能为空的值通过以下参数化可变类型处理:
type option('a) =
| None
| Some('a);
option迫使你总是考虑这种空的情况。 ReasonML是最小程度的支持option。这个可变类型的定义是语言的一部分,但是核心标准库还没有用于处理可选值的实用函数。在此之前,你可以使用BuckleScript's Js.Option。
result 是OCaml中错误处理的另一个标准可变类型:
type result('good, 'bad) =
| Ok('good)
| Error('bad);
在ReasonML的核心库支持它之前,你可以使用BuckleScript's Js.Result。
使用树这种数据结构是ML风格语言的优势之一。这就是为什么它们经常用于涉及语法树(解释器,编译器等)的程序中。例如,Facebook的语法检查器Flow是用OCaml编写的。 因此,作为最后一个例子,我们实现一个简单整数表达式的求值器。 以下是整数表达式的数据结构。
type expression =
| Plus(expression, expression)
| Minus(expression, expression)
| Times(expression, expression)
| DividedBy(expression, expression)
| Literal(int);
这是用这个可变类型编码的表达式:
/* (3 - (16 / (6 + 2)) */
let expr =
Minus(
Literal(3),
DividedBy(
Literal(16),
Plus(
Literal(6),
Literal(2)
)
)
);
最后,这是整型表达式求值的函数。
let rec eval(e: expression) =
switch e {
| Plus(e1, e2) => eval(e1) + eval(e2)
| Minus(e1, e2) => eval(e1) - eval(e2)
| Times(e1, e2) => eval(e1) * eval(e2)
| DividedBy(e1, e2) => eval(e1) / eval(e2)
| Literal(i) => i
};
eval(expr); /* 1 */
扫码关注w3ctech微信公众号
共收到0条回复