原文:http://2ality.com/2018/01/records-reasonml.html
翻译:ppp
系列文章目录详见: “什么是ReasonML?”
记录类似于元组:记录有固定的长度,每个部分可以是不同的类型,并且能直接访问;但不同的是,在一个元组中(它的元件)是按位置访问的,而一个记录(它的字段)是按名称访问的。默认情况下,记录是不可变的。
在你使用记录前,你必须先定义记录的类型,例如:
type point = {
x: int,
y: int, /* 结尾的逗号可选 */
};
我们定义了有x,y两个字段的记录类型point;字段的名字必须以小写字母开头。
在同一个类型的定义域中,字段名不可重复。这个限制是因为记录的类型是靠字段名来确定的(译者注:在不显示声明类型的时候,ReasonML会根据字段名来推断类型)。为了实现这个任务,每个字段名称只能与一个记录类型相关联。
其实可以在多条记录中使用相同的字段名称,但可用性会受到影响:最后一个使用wins作为字段名的类型,将会成为被推断出来的类型。因此,使用其他记录类型变得更加复杂,所以我更喜欢假装不能使用相同的字段名。
稍后我们将研究如何解决此限制。
(译者注:这段你可能会疑惑,举个例子就明白了
type point = {
x: int,
y1: int,
};
type point2 = {
x:int,
y:int,
};
let a = {x:1,y:2};
/*没有显示指定a的类型,只能推断a的类型,这时候a的类型会被推断为point2*/
)
可以嵌套定义记录类型吗?例如,我们可以这样做吗?
type t = { a: int, b: { c: int }};
答案是不能。会抛出一个语法错误。正确定义的定义应该是这样:
type b = { c: int };
type t = { a: int, b: b };
对于b: b,字段名和字段值是一样的。那么你可以把它们缩写为b。这就是所谓的双关语:
type t = { a: int, b };
我们尝试创建一个记录:
# let pt1 = { x: 12, y: -2 };
let pt1: point = {x: 12, y: -2};
请注意字段名称是如何被用来推断pt1具有该类型的point。
还有一种简便的定义方法:
let x = 7;
let y = 8;
let pt2 = {x, y};
/* Same as: { x: x, y: y } */
字段值通过点(.)运算符访问:
# let pt = {x: 1, y: 2};
let pt: point = {x: 1, y: 2};
# pt.x;
- : int = 1
# pt.y;
- : int = 2
记录是不可修改的。要更改记录r的f字段的值,我们必须创建一条新记录s。给s.f赋予新的价值,s其他字段都和r一样。通过以下语法实现:
let s = {...r, f: newValue}
...
称为扩展运算符。他们必须放在前面,且最多使用一次。但是,你可以更新多个字段(而不只是一个字段f)。
这是使用扩展运算符的一个例子:
# let pt = {x: 1, y: 2};
let pt: point = {x: 1, y: 2};
# let pt' = {...pt, y: 3};
let pt': point = {x: 1, y: 3};
所有常用的模式匹配机制也适用于记录。例如:
let isOrig = (pt: point) =>
switch pt {
| {x: 0, y: 0} => true
| _ => false
};
这是如何通过let解构:
# let pt = {x: 1, y: 2};
let pt: point = {x: 1, y: 2};
# let {x: xCoord} = pt;
let xCoord: int = 1;
你可以使用简写定义:
# let {x} = pt;
let x: int = 1;
参数的解构也可以:
# let getX = ({x}) => x;
let getX: (point) => int = <fun>;
# getX(pt);
- : int = 1
在模式匹配过程中,默认情况下,你可以省略所有你不感兴趣的字段。例如:
type point = {
x: int,
y: int,
};
let getX = ({x}) => x; /* Don’t do this */
对于getX(),我们对y并不感兴趣,只设计x字段。但是,最好显示的表现出省略了字段:
let getX = ({x, _}) => x;
x后面的下划线告诉ReasonML:我们忽略了其他的字段。 为什么显示的表明忽略更好?因为现在你可以通过把以下条目添加到bsconfig.json,让RationalML向你提供有关缺少字段名称的警告:
"warnings": {
"number": "+R"
}
初始版本现在触发以下警告:
Warning number 9
4 │ };
5 │
6 │ let getX = ({x}) => x;
the following labels are not bound in this record pattern:
y
Either bind these labels explicitly or add '; _' to the pattern.
我建议更进一步,如果缺少的字段将会抛出异常(编译无法完成):
"warnings": {
"number": "+R",
"error": "+R"
}
有关配置警告的更多信息,请参阅BuckleScript手册。
检查缺少的字段对于使用所有当前字段的代码尤其重要:
let string_of_point = ({x, y}: point) =>
"(" ++ string_of_int(x) ++ ", "
++ string_of_int(y) ++ ")";
string_of_point({x:1, y:2});
/* "(1, 2)" */
如果你要给point在添加另一个字段(比如说z),并希望ReasonML给出关于string_of_point的警告,以便你可以更新它。
变型是我们已经见过的递归类型的第一个例子。你也可以在递归定义中使用记录。例如:
type intTree =
| Empty
| Node(intTreeNode)
and intTreeNode = {
value: int,
left: intTree,
right: intTree,
};
该变型intTree递归地依赖于记录类型intTreeNode的定义。这是如何创建类型的元素intTree:
let t = Node({
value: 1,
left: Node({
value: 2,
left: Empty,
right: Node({
value: 3,
left: Empty,
right: Empty,
}),
}),
right: Empty,
});
在ReasonML中,类型可以通过类型变量进行参数化。定义记录类型时可以使用这些类型变量。例如,如果我们希望树包含任意值,而不仅仅是整数,我们可以让字段为多态类型(行A):
type tree('a) =
| Empty
| Node(treeNode('a))
and treeNode('a) = {
value: 'a, /* A */
left: tree('a),
right: tree('a),
};
每个记录都是在一个作用域内定义的(例如,一个模块)。其字段名称位于该作用域的顶层。虽然这有助于类型推断,但它使得使用字段名称比其他许多语言更复杂。让我们看看如果我们将point放入另一个模块M中,各种与记录相关的机制如何受到影响:
module M = {
type point = {
x: int,
y: int,
};
};
如果我们试图在同一个作用域中再创建一个point类型的记录,会抛出异常:
let pt = {x: 3, y: 2};
/* Error: Unbound record field x */
原因是,x与y不存在于当前作用域内,而是存在于模块M中。
解决这个问题的方法之一是通过在字段名称前加前缀:
let pt1 = {M.x: 3, M.y: 2}; /* OK */
let pt2 = {M.x: 3, y: 2}; /* OK */
let pt3 = {x: 3, M.y: 2}; /* OK */
解决这个问题的另一种方法是对整个记录加上前缀。ReasonML如何显示推断的类型很有趣 - 类型和第一个字段名称都加了前缀:
# let pt4 = M.{x: 3, y: 2};
let pt4: M.point = {M.x: 3, y: 2};
最后,你还可以open模块M,这样把x和y导入到当前作用域。
open M;
let pt = {x: 3, y: 2};
如果你未open模块M,则不能用非限定名称的方式访问这些字段:
let pt = M.{x: 3, y: 2};
print_int(pt.x);
/*
Warning 40: x was selected from type M.point.
It is not visible in the current scope, and will not
be selected if the type becomes unknown.
*/
如果你给字段x加上前缀,警告消失:
print_int(pt.M.x); /* OK */
局部open M也可以:
M.(print_int(pt.x));
print_int(M.(pt.x));
使用模式匹配时,你会遇到与正常访问字段相同的问题 - 如果你不给point的字段加上前缀,你就无法访问他们:
# let {x, _} = pt;
Error: Unbound record field x
如果我们给x加上前缀,就好了:
# let {M.x, _} = pt;
let x: int = 3;
但是,如果给整个模式加上前缀有不行:
# let M.{x, _} = pt;
Error: Syntax error
我们在let绑定时局部open M。我们必须另外将它包在一个代码块(花括号)中,同时用圆括号括起来,这样才是一个合法的表达式:
M.({
let {x, _} = pt;
···
});
我最初说我们可以在多个记录中使用相同的字段名称。实现这一点的小技巧是把每个记录都放在一个单独的模块中。例如,我们定义了两种记录类型,Person.t和Plant.t都有字段t。但它们都是在单独的模块中,不存在名称冲突的问题:
module Person = {
type t = { name: string, age: int };
};
module Plant = {
type t = { name: string, edible: bool };
};
在JavaScript中,有两种方式可以访问一个字段(在JavaScript中称为属性):
// Static field name (known at compile time)
console.log(obj.prop);
function f(obj, fieldName) {
// Dynamic field name (known at runtime)
console.log(obj[fieldName]);
}
在RationalML中,字段名称始终是静态的。JavaScript对象扮演着两个角色:他们既是记录又是字典。在ReasonML中,如果你需要记录,请使用Record,如果你需要字典,请使用Map。
扫码关注w3ctech微信公众号
共收到0条回复