w3ctech

[ReasonML] - Lists and arrays - 列表与数组


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

翻译:ppp

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


在这篇文章中,我们看看ReasonML的两个数据结构 - 列表和数组:

  • 列表是一种不可修改的数据结构,用于存放一个数据的序列,每个元素都是相同类型。它特别适合通过模式匹配进行处理。
  • 数组是一中可修改的数据类型,同于存放可随机访问的数据,每个元素都是相同的类型。它特别适合大数据或需要随机访问。

下表比较了两种数据结构。 | | Lists | Arrays | |------------|-------------| -------| | Size | small–medium | small–large | | Resizable? | flexible | fixed | | Mutability | immutable | mutable | | Elem types | same | same | | Access via | destructuring | index | | Fastest | prepend/remove first | read/write elems |

1. 快速探索:类型签名(类型)

在这篇文章中,你会看到许多类型签名,例如:

let map: ('a => 'b, list('a)) => list('b);

这个类型签名对你来说似乎仍然很神秘。但是通过学习如何读懂它们,你将会有不少收获。让我们来看看map的类型签名告诉了我们什么 - 当我们还不知道map是什么,也不知道它有什么功能时(这将在后面解释)。一步一步理解:

  • let map: («parameters») => «result»; map 是一个函数。
  • 'a => 'b map第一个参数是一个函数,传入类型'a并返回类型'b。撇号表示'a并且'b是类型变量:它们接受任何类型。但是一旦接受了一个特定的类型,它就只能接受这种类型了。
  • list('a) map第二个参数是一个元素类型为'a的列表。
  • list('b) map返回值是一个元素类型是'b的列表。

2. 用于列表和数组的标准库功能

ReasonML目前有两个用于列表和数组的标准模块:

  • List模块的函数,例如: let map: ('a => 'b, list('a)) => list('b);
  • Array模块的函数,例如: let map: ('a => 'b, array('a)) => array('b);

这两个模块里的每个功能都有参数别名的版本,对应下面的这两个模块:

  • ListLabels模块对应的函数: let map: (~f: 'a => 'b, list('a)) => list('b);
  • ArrayLabels模块对应的函数: let map: (~f: 'a => 'b, array('a)) => array('b);

有个小技巧,你可以通过open StdLabels来将标准模块替换为标准模块的参数别名版本:

open StdLabels;

该模块还有全局ArrayLabels的别名:Array,Bytes,List和String等等。因此,如果你open了这个模块,你会覆盖当前的Array等的实现。

3. 列表

ReasonML中的列表是一种典型的功能性数据结构,因为它们的类型是递归定义的,并且列表不可变。让我们看看这是什么意思。

3.1 列表的结构

通过自递归参数化实现的列表

list是一个自递归参数化变体。如果你要自己定义它,这就是它的样子:

type mylist('a) =
  | Nil;
  | Cons('a, mylist('a))

取名Nil和Cons(“构造”)是有历史渊源的,它源于Lisp这种编程语言。通过嵌套Cons创建列表:

# let abc = Cons("a", Cons("b", Cons("c", Nil)));
let abc: mylist(string) = Cons("a", Cons("b", Cons("c", Nil)));

mylist有一个类型的参数'a。它将和mylist递归的传递给Cons。这意味着两件事:首先,mylist的元素可以是任意类型。其次,它们都必须具有相同的类型。通过之前的学习,你可以看到ReasonML自动推断出abc的类型,'a是string。

abc的是实现是一个单向链表。在内存中,它可能看起来像这样:

singly-linked-list.jpg

一个cons对的两部分被称为:

  • 头(或第一个):这是当前链表的第一个值。
  • 尾(或剩下的):它指向链表剩下的部分。

ReasonML列表的创建和模式匹配

ReasonML的列表有特殊的语法。有两种构造方式:

  • 空列表[]
  • cons对[head, ...tail]

因此,模式匹配就像这样:

switch myList {
| [] => ···
| [head, ...tail] => ···
}

这是用另一种方式重新创建上一节中abc的方法:

# let abc = ["a", ...["b", ...["c", ...[]]]];
let abc: list(string) = ["a", "b", "c"];

你可以看到rtop给出了一个等价的,但更紧凑的语法:

# let abc = ["a", "b", "c"];
let abc: list(string) = ["a", "b", "c"];

我们用模式匹配来计算任何列表的长度myList:

let rec len = (myList: list('a)) =>
  switch myList {
  | [] => 0
  | [_, ...tail] => 1 + len(tail)
  };

我们回顾这两个构造函数:

  • 一个空列表的长度为0。
  • 非空列表的长度为:尾部的长度 + 1。

类型参数'a使函数len变得通用。但是在列表这种数据结构中,我们从来不关心这些元素的类型。下面是各种类型的列表使用len函数的示例:

# len([]);
- : int = 0
# len(["a", "b"]);
- : int = 2
# len([1, 2, 3, 4]);
- : int = 4

打印列表

ReasonML内置不支持打印复杂数据结构。但是BuckleScript可以让你使用JavaScript的console.log()。最好像这样调用:

Js.log(Array.of_list(myList));

在我们打印列表myList之前,我们将它转​​换为一个数组。但为什么要这样做呢?因为这可以让输出的效果更好。ReasonML列表在JavaScript中表示为嵌套的有两个元素的数组。

创建列表的其他方式

三点构造函数也被称为展开运算符。该运算符允许你在现有列表之前添加零个或多个元素:

# [...["a", "b"]];
- : list(string) = ["a", "b"]
# ["a", ...["b", "c"]];
- : list(string) = ["a", "b", "c"]
# ["a", "b", ...["c", "d"]];
- : list(string) = ["a", "b", "c", "d"]

可惜的是,在ReasonML中,你只能用在表达式的最后使用,不像JavaScript中,可以在任何地方使用。

# [...["a", "b"], ...["c", "d"]];
Error: Syntax error

ReasonML有自己的连接列表的运算符:

# ["a", "b"] @ ["c", "d"];
- : list(string) = ["a", "b", "c", "d"]

请注意,连接列表操作相对较慢,因为你必须将第一个列表的每个元素添加到第二个列表中:

let rec append = (l1: list('a), l2: list('a)) =>
  switch l1 {
  | [] => l2
  | [head, ...tail] => [head, ...append(tail, l2)]
  };

(这个实现可以被改进,我们将在即将发布的博客文章中看到。)

这是你如何使用append:

# append([1,2,3], [4,5]);
- : list(int) = [1, 2, 3, 4, 5]

这是再次用到了类型推断:

  • 首先,ReasonML推断1这些值的类型int。
  • 然后它推断出第一个输入列表的类型为list(int):
    # [1,2,3];
    - : list(int) = [1, 2, 3]
    
  • 然后它检查l1、l2具有相同的类型参数值'a。
  • 最后,最后根据l1和l2的类型推断出了append的结果的类型list(int)。

3.2 示例:创建列表

range() 创建一个int列表

/**
 * Compute a list of integers starting with `start`,
 * up to and excluding `end_`.
 */
let rec range = (start: int, end_: int) =>
  if (start >= end_) {
    [];
  } else {
    [start, ...range(start + 1, end_)];
  };

在ReasonML中end是一个关键词,因此不是一个合法的变量名。所以给end_加了一个下划线。让我们试试执行range():

# range(0, 0);
- : list(int) = []
# range(0, 1);
- : list(int) = [0]
# range(0, 5);
- : list(int) = [0, 1, 2, 3, 4]

fill()创建一个填充值为~element的列表:

/**
 * Create a list of length `~length` where each
 * element is `~element`.
 */
let rec fill = (~element: 'a, ~length: int) =>
  if (length <= 0) {
    [];
  } else {
    [element, ...fill(~element, ~length=length-1)];
  };

ReasonML使用~element的类型来推断出结果的类型:

# fill("x", 4);
- : list(string) = ["x", "x", "x", "x"]
# fill(0, 3);
- : list(int) = [0, 0, 0]

3.3 示例:遍历列表

计算一个列表的总和

summarize() 计算列表中所有整数的总和:

/**
 * Compute the sum of all the ints in the list `l`.
 */
let rec summarize = (l: list(int)) =>
  switch l {
  | [] => 0
  | [head, ...tail] => head + summarize(tail)
  };

summarize([]); /* 0 */
summarize([3]); /* 3 */
summarize([1, 2, 3]); /* 6 */

访问第n个列表元素

getElementAt() 通过索引检索列表元素:

/**
 * Get the list element at index `~index`.
 * The head of a list has the index 0,
 * the head of its tail the index 1, etc.
 */
let rec getElementAt = (~index: int, l: list('a)) =>
  switch l {
  | [] => None
  | [head, ...tail] =>
    if (index <= 0) {
      Some(head);
    } else {
      getElementAt(~index=index-1, tail);
    }
  };

我们可以用when字句和一个额外的case来取代switch中的if-else,下面的实现更扁平,更易读:

let rec getElementAt = (~index: int, l: list('a)) =>
  switch l {
  | [] => None
  | [head, ..._] when index <= 0 => Some(head)
  | [head, ...tail] => getElementAt(~index=index-1, tail)
  };

代码中有一些值得注意的:

  • 失败的处理是通过变体类型实现的:
    • None 意味着检索失败。
    • Some(x)意味着检索成功,结果为x。
  • 索引0指当前的头(第二种情况)。
  • 当访问到list的结尾[]时,将返回None。l为空或者~index比l的长度还要大时都会触发switch的第一个条件。

标准库中ListLabels.nth()的功能和getElementAt()类似,但它有可能会抛出非法索引的异常,因为它没有使用option。

3.4 示例:更改列表

由于列表是不可修改的 - 你如何改变它们?为了找到答案,我们看看目前为止我们已经看到了两类算法:

  • 递归的遍历列表。例如:len(),summarize(),等。
  • 递归的创建列表。例如:range(),fill(),等。

要更改列表,我们需要将两种方法结合起来:根据需要,我们创建一个全新的列表,并且把数据从已有的列表或数据源中导入。

removeAll()

以下是更改现有列表的函数的第一个示例。

/**
 * Remove all elements from the list `l` that are
 * equal to `~value`.
 */
let rec removeAll = (~value: 'a, l: list('a)) =>
  switch l {
  | [] => []
  | [head, ...tail] when head == value => removeAll(~value, tail)
  | [head, ...tail] => [head, ...removeAll(~value, tail)]
  };

第一种情况意味着我们完成了操作。第三种情况是复制现有列表。第二种情况则是删除等于~value的元素。

这是removeAll()在实际中的使用:

# removeAll(~value=9, [1,9,2,9,3]);
- : list(int) = [1, 2, 3]

replaceAll()

replaceAll() 用于替换值:

/**
 * Inside the list `l`, remove all occurrences of the value `~value`
 * with the value `~with_`.
 */
let rec replaceAll = (~value: 'a, ~with_: 'a, l: list('a)) =>
  switch l {
  | [] => []
  | [head, ...tail] when head == value =>
    [with_, ...replaceAll(~value, ~with_, tail)]
  | [head, ...tail] =>
    [head, ...replaceAll(~value, ~with_, tail)]
  };

第一种情况意味着我们完成了操作。第三种情况是复制现有列表。第二种情况则是执行替换。

我们可以通过一个内联辅助函数replaceOne(),让replaceAll()变得更加紧凑:

let rec replaceAll = (~value: 'a, ~with_: 'a, l: list('a)) => {
  let replaceOne = (x) => if (x == value) with_ else x;
  switch l {
  | [] => []
  | [head, ...tail] =>
    [replaceOne(head), ...replaceAll(~value, ~with_, tail)]
  };
};

replaceAll()实际场景中的使用:

# replaceAll(~value=1, ~with_=11, [1, 2, 1, 3]);
- : list(int) = [11, 2, 11, 3]

drop()

drop() 功能是删除列表元素

/**
 * Remove the first `~count` elements of `theList`.
 */
let rec drop = (~count, theList: list('a)) =>
  switch theList {
  | [] => []
  | l when count <= 0 => l
  | [_, ...tail] => drop(~count=count-1, tail)
  };

让我们来调用drop():

# drop(~count=0, ["a", "b", "c", "d"]);
- : list(string) = ["a", "b", "c", "d"]
# drop(~count=2, ["a", "b", "c", "d"]);
- : list(string) = ["c", "d"]
# drop(~count=2, ["a", "b"]);
- : list(string) = []
# drop(~count=2, ["a"]);
- : list(string) = []
# drop(~count=2, []);
- : list('a) = []

对于最后一个drop()调用的结果,ReasonML无法推断出元素的类型,因此给出一个未对类型绑定的'a类型。

4. 列表的标准库函数

ReasonML的标准库仍在不断变化。因此,我们只在这里看一些重要的。你可以阅读ListLabels的文档来了解其他的功能(如当前存在的内容)。

4.1 ListLabels.map()

类型签名:

let map: (~f: 'a => 'b, list('a)) => list('b);

map()接收类型元素为'a的列表,将函数~f应用于列表的每个元素,并将结果保存在另一个列表中返回。

# ListLabels.map(~f=x => int_of_string(x), ["7", "15", "6"]);
- : list(int) = [7, 15, 6]

该功能是处理数据列表的常用方法。

mapi()是一个map()的另一个版本,它会把元素值和索引都传递给~f,我们可以用mapi()来实现对列表的非破坏性更新:

/**
 * Create a copy of `theList` whose element at index `~index`
 * is `~value`.
 */
let setElementAt = (~index: int, ~value: 'a, theList: list('a)) =>
  ListLabels.mapi(
    ~f=(i,x) => if (i==index) value else x,
    theList
  );

~f函数只对索引为~index的元素做了修改。

setElementAt()的实际使用:

# setElementAt(~index=1, ~value="|", ["a", "b", "c"]);
- : list(string) = ["a", "|", "c"]

4.2 ListLabels.filter()

这是该函数的类型签名:

let filter: (~f: 'a => bool, list('a)) => list('a);

filter()把列表的每个元素传递给~f。如果~f返回true,该元素被将包含在返回结果中,如果是false,则不包含。用法如下:

# ListLabels.filter(~f=x=>x>5, [8, 4, 9, 7, 2]);
- : list(int) = [8, 9, 7]

4.3 ListLabels.for_all()

类型签名:

let for_all: (~f: 'a => bool, list('a)) => bool;

如果列表中的每个元素在函数~f都返回true,for_all()返回就true。例如:

# ListLabels.for_all(~f=x=>x>3, [4,5,6]);
- : bool = true
# ListLabels.for_all(~f=x=>x>3, [3,4,5,6]);
- : bool = false

如果~f返回false,for_all就回立刻返回。而且结果一定为false。for_all是以数学运算符∀来命名的。

ListLabels.exists()和for_all()类似:只要列表中有一个元素使回调函数的返回值为true,则返回true。exists以数学运算符∃命名。

4.4 ListLabels.flatten()

类型签名:

let flatten: list(list('a)) => list('a);

flatten()通过连接元素的方式把多个列表转化为一个列表。也就是说,以下三个表达式是等价的:

flatten([l1, l2, l3])
ListLabels.append(l1, ListLabels.append(l2, l3))
l1 @ l2 @ l3

这是flatten()的用法:

# ListLabels.flatten([[1,2], [], [3,4,5]]);
- : list(int) = [1, 2, 3, 4, 5]

如果你想使用任意嵌套的列表,请回想一下,在ReasonML中,所有元素必须具有相同的类型。因此,如果一个列表元素本身就是一个列表,那么所有元素都必须是列表:

# ListLabels.flatten([[1,[2]], [], [3]]);
Error: This expression has type list('a)
but an expression was expected of type int
# ListLabels.flatten([[[1],[2]], [], [[3]]]);
- : list(list(int)) = [[1], [2], [3]]

让我们继续查看flatten()的用例。

用例:同时过滤和映射

flatten()让你可以同时过滤和映射。例子,考虑这种情况:尝试提取存储在一个列表中的多个列表的第一个元素。你可以:

  • 首先通过ListLabels.filter()过滤出空列表(没有第一个元素)。
  • 然后通过ListLabels.map()将每个非空列表映射到其头部。

或者你可以使用flatten同时执行这两种操作:

module L = ListLabels;
let listFromHead = (l: list('a)) =>
  switch (l) {
  | [] => []
  | [head, ..._] => [head]
  };
let heads = (l: list(list('a))) =>
  L.flatten(L.map(~f=listFromHead, l));

首先,我们将每个非空列表通过它的头映射到另一个列表中,每个空列表映射到一个空列表。然后我们把结果扁平化。看起来像这样:

# let l0 = [[1, 2], [], [3,4,5]];
let l0: list(list(int)) = [[1, 2], [], [3, 4, 5]];
# L.map(~f=listFromHead, l0);
- : list(list(int)) = [[1], [], [3]]
# let l1 = L.map(~f=listFromHead, l0);
let l1: list(list(int)) = [[1], [], [3]];
# L.flatten(l1);
- : list(int) = [1, 3]

这些步骤等价于:

# heads([[1, 2], [], [3,4,5]]);
- : list(int) = [1, 3]

把listFromHead和getHead做一个对比,这对我们有帮助。

let getHead = (l: list('a)) =>
  switch (l) {
  | [] => None
  | [head, ..._] => Some(head)
  };

也就是说,None表示“l没有头”:

# getHead(["a", "b"]);
- : option(string) = Some("a")
# getHead([1, 2, 3]);
- : option(int) = Some(1)
# getHead([]);
- : option('a) = None

listFromHead中,我们使用空列表而不是None,以及用只有一个元素的列表取代Some。

用例:映射到多个值

假设我们已经创建了一个person的列表和节点:

type person = Person(string, list(string));
let persons = [
  Person("Daisy", []),
  Person("Della", ["Huey", "Dewey", "Louie"]),
  Person("Marcus", ["Minnie"])
];

如果我们想获取列表的节点,ListLabels.map()几乎满足了我们的需求,但不是很完美:

# ListLabels.map(~f=(Person(_, ch)) => ch, persons);
- : list(list(string)) = [[], ["Huey", "Dewey", "Louie"], ["Minnie"]]

可惜的是,这是字符串列表的列表,而不是字符串列表。我们可以通过ListLabels.flatten()来解决这个嵌套列表的问题:

let collectChildren = (persons: list(person)) =>
  ListLabels.flatten(
    ListLabels.map(
        ~f=(Person(_, children)) => children,
        persons));

collectChildren(persons);
  /* ["Huey", "Dewey", "Louie", "Minnie"] */

现在我们得到了期望的结果:

# collectChildren(persons);
- : list(string) = ["Huey", "Dewey", "Louie", "Minnie"]

用例:有条件地插入值

有时,你会根据条件创建列表,其中添加或省略了一些元素(下示例中的cond):

let func = (cond: bool) => ListLabels.flatten([
  if (cond) ["a"] else [],
  [
    "b",
    "c"
  ]
]);

这是如何使用func():

# func(true);
- : list(string) = ["a", "b", "c"]
# func(false);
- : list(string) = ["b", "c"]

4.5 ListLabels.fold_left()

类型签名:

let fold_left: (~f: ('a, 'b) => 'a, ~init: 'a, list('b)) => 'a;

fold_left() 工作如下:

  • 输入:类型为list('b)的列表(最后一个参数)
  • 结果:类型的值'a

fold_left()的计算结果取决于它的参数:函数~f。输入参数的每个元素都会传入~f:

let nextIntermediateResult = f(intermediateResult, elem);

intermediateResult是求得值。第一个中间结果是~init。最后的nextIntermediateResult就是fold_left()的结果。

我们来看一个具体的例子。

fold_left()通过例子:summarize()

我们已经见过summarize()函数,用于计算整数列表的和。让我们通过fold_left()实现该功能:

let rec summarize = (l: list(int)) =>
  ListLabels.fold_left(~f=(r, elem) => r + elem, ~init=0, l);

要理解summarize()是如何工作的,请先看看以下表达式:

summarize([1,2,3]) /* 6 */

为了得到结果6,将采取以下步骤:

/* Parameter */
let f = (r, elem) => r + elem;
let init = 0;

/* Steps */
let result0 = f(init, 1); /* 1 */
let result1 = f(result0, 2); /* 3 */
let result2 = f(result1, 3); /* 6 */

result2就是fold_left()的结果。

fold_left()的另一种理解

fold_left()的另一种理解是采用二元运算符~f并将其变为列表的n元运算符。在数学中也有从二元运算到n元运算的一个例子:二元运算符+的n元版本是运算符Σ。summarize()就相当于从+变成了Σ。它也可以这样写:

# ListLabels.fold_left(~f=(+), ~init=0, [1, 2, 3]);
- : int = 6

我发现fold_left最容易理解的是它是工作在这种模式下 - ~f参数是一个和位置无关的参数(参数顺序无关紧要)。你可以用它做很多事情 - 继续看下个例子。

一个更复杂的例子:查找值

该函数contains()使用它在列表中查找值:

let contains = (~value: 'a, theList: list('a)) => {
  let f = (found, elem) => found || elem == value;
  fold_left(~f, ~init=false, theList);
};

4.6 通过iteri()将列表转换为数组

类型签名:

let iteri: (~f: (int, 'a) => unit, list('a)) => unit;

iteri()将遍历列表的每个元素,并传给~f,参数是元素的值和索引,它的返回为unit。这意味着iteri并没有实际的功能,具体的操作都是通过函数~f来实现的。

以下函数使用iteri()来填充一个数组。通过逐个写入数组的元素来实现:

let arrayFromList = (~init: 'a, l: list('a)) => {
  let arr = ArrayLabels.make(ListLabels.length(l), init);
  ListLabels.iteri(~f=(i, x) => arr[i]=x, l);
  arr;
};

~init是必选参数,因为make()需要它(为什么稍后解释)。

arrayFromList()的实际用例:

# arrayFromList(~init=0, [1,2,3]);
- : array(int) = [|1, 2, 3|]

5. 数组

数组很像列表:它们的所有元素都具有相同的类型,并且可以按位置访问它们。但他们也不尽相同:

  • 数组是可变的。列表是不可变的。
  • 数组不能调整大小。但用列表,你可以有效地添加元素并检索列表尾部。(BuckleScript可以让你调整数组的大小,但如果你这样做的话,你将失去跨平台兼容性。)
  • 数组提供快速索引访问。列表通过递归和模式匹配进行处理。

5.1 创建数组

以下小节介绍创建数组的三种常用方法。

数组字面量

# [| "a", "b", "c" |];
- : array(string) = [|"a", "b", "c"|]

ArrayLabels.make()

类型签名:

let make: (int, 'a) => array('a);

第一个参数指定数组的长度。第二个参数指定了要填充的值。为什么第二个参数是必选的的?因为make()返回的数组必须只包含类型'a的值。ReasonML没有null,所以你必须手动的为'a指定一个类型。

这是make()如何工作的:

# ArrayLabels.make(3, "x");
- : array(string) = [|"x", "x", "x"|]
# ArrayLabels.make(3, true);
- : array(bool) = [|true, true, true|]

ArrayLabels.init()

类型签名:

let init: (int, ~f: int => 'a) => array('a);

第一个参数指定数组的长度。函数~f将根据索引映射出对应的初始值。例如:

# ArrayLabels.init(~f=i=>i, 3);
- : array(int) = [|0, 1, 2|]
# ArrayLabels.init(~f=i=>"abc".[i], 3);
- : array(char) = [|'a', 'b', 'c'|]

5.2 获取数组的长度

ListLabels.length()返回数组的长度:

# ArrayLabels.length([| "a", "b", "c" |]);
- : int = 3

5.3 读取和写入数组元素

这就是读取和写入数组元素的方式:

# let arr = [| "a", "b", "c" |];
let arr: array(string) = [|"a", "b", "c"|];
# arr[1]; /* read */
- : string = "b"
# arr[1] = "x"; /* write */
- : unit = ()
# arr;
- : array(string) = [|"a", "x", "c"|]

5.4 模式匹配和数组

模式匹配数组类似于匹配元组,而不是匹配列表。让我们从元组和列表开始(我们可以忽略exhaustiveness警告,因为我们正在处理固定数据):

# let (a, b) = (1, 2);
let a: int = 1;
let b: int = 2;
# let [a, ...b] = [1, 2, 3];
Warning: this pattern-matching is not exhaustive.
let a: int = 1;
let b: list(int) = [2, 3];

接下来我们将解构一个数组:

# let [| a, b |] = [| 1, 2 |];
Warning: this pattern-matching is not exhaustive.
let a: int = 1;
let b: int = 2;

与元组类似,模式必须与数据具有相同的长度(这是例外情况):

# let [| a, b |] = [| 1, 2, 3 |];
Warning: this pattern-matching is not exhaustive.
Exception: Match_failure

5.5 列表和数组之间的转换

这就是如何在列表和数组之间进行转换:

  • 从数组到列表(ArrayLabels 模块): let to_list: array('a) => list('a);
  • 从列表到数组(ArrayLabels 模块): let of_list: list('a) => array('a);

有时候使用数组处理数据会比列表更方便,然后你又可以再把它转换为列表(如果需要的话,再转换回数组)。

5.6 处理数组

标准库仍处于不断变化之中。因此,这里只会演示一些重要的方法。

ArrayLabels.map()

map()对于数组的处理和列表是类似的:

# ArrayLabels.map(s => s ++ "x", [| "a", "b" |]);
- : array(string) = [|"ax", "bx"|]

ArrayLabels.fold_left()

fold_left()也与列表的类似:

let maxOfArray = (arr) =>
  ArrayLabels.fold_left(~f=max, ~init=min_int, arr);

这是maxOfArray()的用法:

# maxOfArray([||]);
- : int = -4611686018427387904
# maxOfArray([|3, -1, 5|]);
- : int = 5

这里我又一次见到了从二元到n元的变化 max() -> maxOfArray()。对max()而言,我们还使用了整数常量min_int。两者都属于Pervasives模块。

max 是一个适用于大多数类型的二元函数:

# max(1.0, 1.1);
- : float = 1.1
# max(None, Some(1));
- : option(int) = Some(1)
# max("a", "b");
- : string = "b"
# max(4, -3);
- : int = 4

min_int 是int的最小取值(它的确切值和当前的平台有关):

# min_int;
- : int = -4611686018427387904

通过fold_right()将数组转换为列表

fold_right()的原理和fold_left()类似,但它是从最后一个元素开始的。它的类型签名是:

let fold_right: (~f: ('b, 'a) => 'a, array('b), ~init: 'a) => 'a;

此功能的一个使用场景是将数组转换为列表。该列表必须按照以下方式构建(即,你必须从最后一个数组元素开始):

[··· [x_2nd_last, ...[x_last, ...[]]]]

像这样:

let listFromArray = (arr: array('a)) =>
  ArrayLabels.fold_right(~f=(ele, l) => [ele, ...l], arr, ~init=[]);

这是listFromArray()的实际用例:

# listFromArray([||]);
- : list('a) = []
# listFromArray([| 1, 2, 3 |]);
- : list(int) = [1, 2, 3]
# listFromArray([| "a", "b", "c" |]);
- : list(string) = ["a", "b", "c"]

过滤数组

所有数组的处理函数都会返回与输入数组长度相同的数组。因此,如果你想删除数组的元素,你必须借助列表:

let filterArray = (~f, arr) =>
  arr
  |> ArrayLabels.to_list
  |> ListLabels.filter(~f)
  |> ArrayLabels.of_list;

filterArray() 实例:

# filterArray(~f=x=>x>0, [|-2, 3, -4, 1|]);
- : array(int) = [|3, 1|]
w3ctech微信

扫码关注w3ctech微信公众号

共收到0条回复