w3ctech

[ReasonML] - Records - 记录类型


原文:http://2ality.com/2018/01/records-reasonml.html

翻译:ppp

系列文章目录详见: “什么是ReasonML?


1. 什么是记录?

记录类似于元组:记录有固定的长度,每个部分可以是不同的类型,并且能直接访问;但不同的是,在一个元组中(它的元件)是按位置访问的,而一个记录(它的字段)是按名称访问的。默认情况下,记录是不可变的。

2. 基础用法

2.1 定义记录类型

在你使用记录前,你必须先定义记录的类型,例如:

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 };

2.2 尝试创建一个记录

我们尝试创建一个记录:

# 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 } */

2.3 访问字段值

字段值通过点(.)运算符访问:

# let pt = {x: 1, y: 2};
let pt: point = {x: 1, y: 2};
# pt.x;
- : int = 1
# pt.y;
- : int = 2

2.4 记录的无损更新

记录是不可修改的。要更改记录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};

2.5 模式匹配

所有常用的模式匹配机制也适用于记录。例如:

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的警告,以便你可以更新它。

2.6 递归记录类型

变型是我们已经见过的递归类型的第一个例子。你也可以在递归定义中使用记录。例如:

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,
});

2.7 参数化记录类型

在ReasonML中,类型可以通过类型变量进行参数化。定义记录类型时可以使用这些类型变量。例如,如果我们希望树包含任意值,而不仅仅是整数,我们可以让字段为多态类型(行A):

type tree('a) =
  | Empty
  | Node(treeNode('a))
and treeNode('a) = {
  value: 'a, /* A */
  left: tree('a),
  right: tree('a),
};

3. 其他模块中的记录

每个记录都是在一个作用域内定义的(例如,一个模块)。其字段名称位于该作用域的顶层。虽然这有助于类型推断,但它使得使用字段名称比其他许多语言更复杂。让我们看看如果我们将point放入另一个模块M中,各种与记录相关的机制如何受到影响:

module M = {
  type point = {
    x: int,
    y: int,
  };
};

3.1 通过其他模块创建记录

如果我们试图在同一个作用域中再创建一个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};

3.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));

3.3 模式匹配和来自其他模块的记录

使用模式匹配时,你会遇到与正常访问字段相同的问题 - 如果你不给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;
  ···
});

3.4 在多个记录中使用相同的字段名称

我最初说我们可以在多个记录中使用相同的字段名称。实现这一点的小技巧是把每个记录都放在一个单独的模块中。例如,我们定义了两种记录类型,Person.t和Plant.t都有字段t。但它们都是在单独的模块中,不存在名称冲突的问题:

module Person = {
  type t = { name: string, age: int };
};
module Plant = {
  type t = { name: string, edible: bool };
};

4. 常见问题:记录

4.1 有没有办法动态指定一个字段的名称?

在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微信

扫码关注w3ctech微信公众号

共收到0条回复