w3ctech

[ReasonML] - Functions - 函数


原文:原文:http://2ality.com/2017/12/functions-reasonml.html

翻译:ppp

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


本文详解ReasonML中的函数。

1. 函数的定义

匿名函数定义如下:

(x) => x + 1;

这个函数有一个参数 x,函数体为x+1

你可以通过把这个匿名函数用let绑定(赋值)给一个变量的方式来给这个函数命名。

let plus1 = (x) => x + 1;

然后你就可以这样调用这个函数了:

# plus1(5);
- : int = 6

1.1 函数作为另一个函数的参数(高阶函数)

函数同时也可以作为另一个函数的参数。为了演示这个特性,我们将简单的使用一下List(列表)(这将在后面的文章中详细解释)。简单的说,List可以理解为一个单向链表,类似于不可变的数组(译者注:为什么单向链表会像一个不可变的数组?单向列表也可以可修改的)。

List.map(func, list) 接收一个函数和List作为参数,并把list的每一个元素作为func的参数,然后func的返回结果作为新new_list的元素,最终返回新的new_list。例如:

# List.map((x) => x + 1, [12, 5, 8, 4]);
- : list(int) = [13, 6, 9, 5]

我们把这些函数成为高阶函数:把函数作为参数 或 返回值为函数的函数。其它的函数则称为初阶函数。List.map()是一个高阶函数。plus1()是初阶函数。

1.2 块做为函数体

函数的体是一个表达式。我们已知块也是表达式,所以对plus1来说的以下两个定义是等价的。

let plus1 = (x) => x + 1;

let plus1 = (x) => {
    x + 1
};

1.3 不加括号的单参数函数

如果函数只有一个参数,且参数为一个合法的标识符,那么可以省略括号。

let plus1 = x => x + 1;

1.4 避免未使用参数的警告

ReasonML的编译器和编辑插件会对未使用的变量发出警告。例如:下面的函数就出现 “unused variable y” 的警告。

let getFirstParameter = (x, y) => x;

你可以通过给没有用到的参数加一个下划线的方式来避免这个警告。(译者注:这在定义接口时出 于对今后扩展的考虑常用到)

let getFirstParameter = (x, _y) => x;

你也可以直接用 _ 作为变量名,多次使用都是允许的:

let foo = (_, _) => 123;

2. 通过let rec循环绑定

通常,你只能访问已经通过let绑定即已经存在的值。这意味着你不能通过let来定义互递归函数或自递归函数。

2.1 定义互递归函数

让我们先解释一下互递归函数(参考)。下面的两个函数 evenodd 就是互递归函数。你必须使用专用的关键字"let rec"来定义:

let rec even = (x) =>
  switch x {
  | n when n < 0 => even(-x) /* A */
  | 0 => true /* B */
  | 1 => false /* C */
  | n => odd(n - 1)
  }
and odd = (x) => even(x - 1);

请注意and是如何连接let rec的两个部分的,在and前面没有分号。在最后的那个分号才表明let rec的表达式结束。 evenodd 就这样基于对方的定义而定义。

  • 如果x-1是偶,那么整数 x 就是奇;
  • 如果x-1是奇,那么整数 x 就是偶。

显然只是这样肯定是不行的,所以我们必须定义一些最基础的 case(B/C行)。同时考虑到负数的情况(A行)

# even(11);
- : bool = false
# odd(11);
- : bool = true

# even(24);
- : bool = true
# odd(24);
- : bool = false

# even(-1);
- : bool = false
# odd(-1);
- : bool = true

2.2 定义自递归函数(即我们常说的递归函数)

你同样需要使用let rec来定义自递归,因为函数体里当出现递归调用时,函数的定义都还没完成。例如:

let rec factorial = (x) =>
  if (x <= 0) {
    1
  } else {
    x * factorial(x - 1)
  };

factorial(3); /* 6 */
factorial(4); /* 24 */

3. 术语:arity(参数数量)

函数的arity性质是它的参数的个数。例如,factorial()arity是1。下面是参数个数从0到3的函数对应的名称:

  • A nullary function(零元函数)是没有参数的函数。
  • A unary function(一元函数)是有1个参数的函数。
  • A binary function(二元函数)是有2个参数的函数。
  • A ternary function(三元函数)是有3个参数的函数。

除了3,我们讨论了4元,5元函数,等等。如果函数的arity是可以变化的,叫做可变参函数。

4. 函数的类型

函数是我们接触到的第一种复杂类型:通过组合其它类型而构建的类型。让我们使用ReasonML的命令行rtop来确定两个函数的类型。

4.1 初阶函数的类型

首先,定义一个函数add():

# let add = (x, y) => x + y;
let add: (int, int) => int = <fun>;

所以add的类型是:

(int, int) => int

箭头表明了add是一个函数。它有两个参数,并且有一个int类型的返回值。

(int, int) => int 的这种标记方法也被称作为add函数的类型签名。它描述了函数输入输出的类型。

4.2 高阶函数的类型

然后,定义一个高阶函数callFunc():

# let callFunc = (f) => f(1) + f(2);
let callFunc: ((int) => int) => int = <fun>;

你可以看到callFunc的参数本身就是一个类型为(int) => int的函数。

下面是如何调用callFunc

# callFunc(x => x);
- : int = 3
# callFunc(x => 2 * x);
- : int = 6

4.3 类型声明和类型推断

在一些静态类型的编程语言中,你必须为函数的所有参数和函数的返回值提供类型声明。例如:

# let add = (x: int, y: int): int => x + y;
let add: (int, int) => int = <fun>;

前两个:int声明了参数x,y的类型,最后一个:int声明了返回值的类型。

ReasonML允许你省略返回类型,然后通过参数的类型和函数体推断出返回值得类型:

# let add = (x: int, y: int) => x + y;
let add: (int, int) => int = <fun>;

但是,ReasonML的类型推断比这更复杂。它不只是自上而下的推测,还可以是自下而上的。例如,它可以通过x,y使用了int运算符+来运算,从而推断出xyint类型:

# let add = (x, y) => x + y;
let add: (int, int) => int = <fun>;

换句话说:大部分类型的声明都是可选的。

4.4 类型声明可使类型推断更准确

尽管类型声明是可选的,但提供它们有时也会使类型推断更有效。例如,下面的函数createIntPair:

# let createIntPair = (x, y) => (x, y);
let createIntPair: ('a, 'b) => ('a, 'b) = <fun>;

ReasonML推断出的类型就是一个类型变量类型。这些以'开始的类型,表示是“任何类型”。稍后将对它们进行更详细的解释。

如果我们对参数的类型进行声明,我们就会得到更具体的类型:

# let createIntPair = (x: int, y: int) => (x, y);
let createIntPair: (int, int) => (int, int) = <fun>;

如果只注释返回值,我们也会得到更具体的类型:

# let createIntPair = (x, y): (int, int) => (x, y);
let createIntPair: (int, int) => (int, int) = <fun>;

4.5 最佳实践:声明所有参数的类型

我喜欢函数的编码风格是对所有参数都类型都作出声明,但是让ReasonML推测返回值得类型。除了能让类型检查更有效外,对参数类型的声明也是对代码的一种注释(调用时自动检查参数的一致性)。

5. 所有函数都有参数

ReasonML中没有零元函数,只是在你使用时不容易发现而已。

例如,如果你定义了一个没有参数的函数,ReasonML会为你添加一个类型为unit的参数:

# let f = () => 123;
let f: (unit) => int = <fun>;

函数调用时,虽然你省略了参数,但ReasonML同样会传入()作为函数的参数。

# f();
- : int = 123
# f(());
- : int = 123

下面的例子是这种处理的另一个佐证:如果你调用一个1元函数时不传入参数,rtop会强调()并且抛出表达式有类型的错误,而不是提示说缺失参数。

# let times2 = (x) => x * 2;
let times2: (int) => int = <fun>;
# times2();
Error: This expression has type unit but
an expression was expected of type int

结论:ReasonML没有零元函数,它只是在定义和调用时把默认参数()隐藏了。

5.1 为何没有零元函数?

为什么ReasonML没有零元函数?这是归根于ReasonML对函数的处理是通过偏函数应用(partial application)方式进行的(稍后将详细解释)。其导致的结果是:调用时如果不提供完整的参数,那么将生成并返回一个新函数,这个函数的参数是调用时没有传入的那些剩余的参数。因此,如果你不传入参数,那么func()的返回值将与func相同,也是一个函数。也就是说,执行func()将不会做任何事情。

6. 函数参数的解构

在任何对变量进行绑定的时候都存在解构:存在于let表达式中,同样也存在于函数的参数定义中。后者将在以下的函数得到说明,该函数计算元组中元件的和:

let addComponents = ((x, y)) => x + y;
let tuple = (3, 4);
addComponents(tuple); /* 7 */

((x, y))外面的两层括号说明,addComponents这个函数的参数是一个以x,y作为元件的元组。而并不是有用x,y两个参数的二元函数。函数的类型是:

addComponents: ((int, int)) => int

当你在做函数参数类型声明的时候,你既可以单独对元件进行声明:

# let addComponents = ((x: int, y: int)) => x + y;
let addComponents: ((int, int)) => int = <fun>;

也可以对元组的类型做一个统一的声明:

# let addComponents = ((x, y): (int, int)) => x + y;
let addComponents: ((int, int)) => int = <fun>;

7. 参数别名

到目前为止,我们只接触过位置参数:在调用时由实参的位置决定它将被绑定到(赋值给)哪个与它对应的形参上。

但ReasonML也支持参数别名。在这里,参数别名用于把实参和形参的对应关系做一个映射。

如下例,让我们看下这个增加了参数别名add的实现:

let add = (~x, ~y) => x + y;
add(~x=7, ~y=9); /* 16 */

在这个函数定义中,参数别名~x和参数x都用了x作为它们的名字。你也可以给它们取不同的名字,例如:~x为参数别名而op1为形参名:

let add = (~x as op1, ~y as op2) => op1 + op2;

在调用时,你可以把 ~x=x 缩写为 ~x:

let x = 7;
let y = 9;
add(~x, ~y);

参数别名的一个好处就是你可以不再受参数顺序的约束:

# add(~x=3, ~y=4);
- : int = 7
# add(~y=4, ~x=3);
- : int = 7

7.1 参数别名对函数类型匹的影响

关于参数别名有一个问题需要提醒:由于参数别名可以不受位置约束,这类函数类型只有在参数别名按相同顺序声明时才会成功匹配。 看看以下三个函数。

let add = (~x, ~y) => x + y;
let addxy = (add: ((~x: int, ~y: int) => int)) => add(5, 2);
let addyx = (add: ((~y: int, ~x: int) => int)) => add(5, 2);

addxy正常执行:

# addxy(add);
- : int = 7

但是,执行addyx时,会抛出一个异常,因为参数别名的顺序是错误的:

# addyx(add);
Error: This expression has type
(~x: int, ~y: int) => int
but an expression was expected of type
(~y: int, ~x: int) => int

8. 可选参数

在ReasonML中,只有有别名的参数是可选的。在下面的代码中,x和y都是可选的。

let add = (~x=?, ~y=?, ()) =>
  switch (x, y) {
  | (Some(x'), Some(y')) => x' + y'
  | (Some(x'), None) => x'
  | (None, Some(y')) => y'
  | (None, None) => 0
  };

add(~x=7, ~y=2, ()); /* 9 */
add(~x=7, ()); /* 7 */
add(~y=2, ()); /* 2 */
add(()); /* 0 */
add();   /* 0 */

让我们来研究一下这个相对复杂的代码做了什么。 为什么()作为最后一个参数呢?下一节将对此进行解释。 switch做什么?xy通过=?被声明为可选参数。因此,两者都是可选类型(int)。可选类型本身是一种变体,详情将在即将发表的文章中解释。现在,我将简要介绍一下。可选类型的定义为:

type option('a) = None | Some('a);

当调用add()时,将用到可选类型:

  • 如果你省略了参数~x,那么x将被绑定为None
  • 如果你传入了123作为~x的值,那么x将被绑定为(123)。

换句话说,可选类型是把值封装了起来,而示例中的switch又拆解了它们。(译者注:短短几句说不清,看后续文章吧)

8.1 如果要使用可选参数,你至少需要一个位置参数(必选参数)

为什么在add函数最后要添加unit类型的参数(也可以说是一个空参数)?

let add = (~x=?, ~y=?, ()) =>
  ···

原因与偏函数应用(partial application)有关(稍后将详细解释)。简而言之,这里有两件事是矛盾的:

  • 对于偏函数应用而言,如果省略了参数,那么你讲创建一个只包涵被省略参数的函数。
  • 对于可选参数而已,如果省略了参数,就应该绑定到它们的默认值。

为了解决这个冲突,当遇到第一个必选参数时,ReasonML会为所有缺失的可选参数赋上默认值。但在遇到必选参数之前,ReasonML仍然会按照可选参数处理。也就是说,你总是需要一个必选参数来触发这一操作。由于add()没有必选参数,所以我们添加了一个空。在对函数参数解构时,()模式将会强制把必选参数用()赋值。 添加空参数的另一个原因是,我们将无法触发两个默认值,因为add()add(())相同。 这种稍微有点奇怪的方法的优点是,你可以获得偏函数应用和可选参数两者的好处。

8.2 可选参数的类型声明

当你为可选参数声明类型时,你必须使用option(···):

let add = (~x: option(int)=?, ~y: option(int)=?, ()) =>
  ···

add函数的类型是:

(~x: int=?, ~y: int=?, unit) => int

不幸的是,在这种情况下,函数定义与类型不同。理由是我们需要区分这两样东西:

  • 外部类型:你通常调用add时使用的是int类型的值,而不是可选类型的值。
  • 内部类型:在内部,你需要处理可选类型的值。

在下一节中,我们将使用参数的默认值,这样内部类型就不同了,但是外部类型(也因此add函数的类型)却是相同的。

8.3 参数的默认值

处理缺失的参数是个麻烦事:

let add = (~x=?, ~y=?, ()) =>
  switch (x, y) {
  | (Some(x'), Some(y')) => x' + y'
  | (Some(x'), None) => x'
  | (None, Some(y')) => y'
  | (None, None) => 0
  };

上面这种情况,如果调用时省略了实参,我们只希望x,y被赋予0的默认值。ReasonML为此提供了特殊的语法:

let add = (~x=0, ~y=0, ()) => x + y;

新版本的add函数的用法和之前完全相同:我们把是如何处理缺失函数这件事,通过默认值的方式隐藏在了函数里,对使用者透明。

8.4 参数默认值的类型声明

如果有默认值,那么对参数类型的声明如下: let add = (~x: int=0, ~y: int=0, ()) => x + y; add 的类型为: (~x: int=?, ~y: int=?, unit) => int

8.5 将可选类型值传递给可选参数(进阶)

在内部,可选参数是作为可选类型的元素被接收的(None或Some(x))。到目前为止,你只能通过添加或省略参数来传递这些值。但是,还有一种方法可以直接传递这些值。在我们使用这个特性之前,让我们先通过下面的函数来尝试一下。

let multiply = (~x=1, ~y=1, ()) => x * y;`

multiply有两个可选参数。让我们从传递~x和省略~y开始,通过可选类型的元素:

# multiply(~x = ?Some(14), ~y = ?None, ());
- : int = 14

传递可选类型值的语法为: ~label = ?expression 如果表达式是一个名字和参数别名一样的变量的话,可以缩写为:

~foo = ?foo
~foo?

这两个语法是等价的。 那么有什么用呢?可以把一个函数的可选参数传给另一个函数的可选参数。这样,它就可以用第二个函数的参数默认值,而不用在第一个函数里面处理。 让我们看这个例子:下面的函数square有一个可选参数,它被传递给multiply的两个可选参数:

let square = (~x=?, ()) => multiply(~x?, ~y=?x, ());

square不需要指定参数默认值,它可以使用multiply的默认值。

9. 偏函数应用

偏函数应用是一种使函数更加通用的机制:如果在函数调用f(...)时省略一个或多个参数,则f返回一个新函数,该函数将丢失的参数映射到f的最终结果中。也就是说,在多个步骤中应用f的参数。第一步称为偏函数应用或偏函数调用。(译者注:绕啊,看例子就懂了) 让我们看看它是如何工作的。我们首先创建一个函数并添加两个参数:

# let add = (x, y) => x + y;
let add: (int, int) => int = <fun>;

然后我们部分地调用定义好的二元函数add函数来创建一元函数plus5:

# let plus5 = add(5);
let plus5: (int) => int = <fun>;

我们只传入了add的第一个参数x,当我们调用plus5时,我们提供了add的第二个参数y:

# plus5(2);
- : int = 7

9.1 为什么偏函数应用有用?

偏函数应用允许你编写更紧凑的代码。为了演示如何使用,我们将使用一个List:

# let numbers = [11, 2, 8];
let numbers: list(int) = [11, 2, 8];

接下来,我们将使用标准库函数List.mapList.map(func, myList) - 接收myList,将每个元素作为func的参数,并把func的返回值存放到一个新列表返回。 我们用该方法为list中的每个数字加2:

# List.map(x => add(2, x), numbers);
- : list(int) = [13, 4, 10]

使用偏函数应用,我们可以使代码更加紧凑:

# List.map(add(2), numbers);
- : list(int) = [13, 4, 10]

哪个版本比较好?这取决于你的喜好。第一个版本可以说是逻辑描述更清晰,第二个版本则更简洁。 偏函数应用更大的亮点是与管道运算符(|>)一起使用,用于函数的组合(稍后将对此进行解释)。

9.2 偏函数应用和参数别名

到目前为止,我们见过带必选参数的偏函数应用,它同样也可以使用参数的别名。在来看看带参数别名版本的add函数:

# let add = (~x, ~y) => x + y;
let add: (~x: int, ~y: int) => int = <fun>;

如果调用时值传入第一个别名参数,就会得到一个将第二个参数映射到结果的函数:

# add(~x=4);
- : (~y: int) => int = <fun>

如果只提供第二个别名参数也是类似的。

# add(~y=4);
- : (~x: int) => int = <fun>

也就是说,别名对参数顺序不做强制要求。这意味着使用参数别名可以使得偏函数应用更通用,因为你可以对任何一个参数使用,而不仅仅是最后一个。

偏函数应用和可选参数

我们看看可选参数,以下版本的add只有可选参数:

# let add = (~x=0, ~y=0, ()) => x + y;
let add: (~x: int=?, ~y: int=?, unit) => int = <fun>;

如果你只传入~x或~y,偏函数应用和以前一样(加上unit必选参数),正常运行:

# add(~x=3);
- : (~y: int=?, unit) => int = <fun>
# add(~y=3);
- : (~x: int=?, unit) => int = <fun>

但是,如果你提供了必选参数,则偏函数应用失效,会立刻赋值为默认值:

# add(~x=3, ());
- : int = 3
# add(~y=3, ());
- : int = 3

即使你使用了一个或两个中间步骤,也还是需要()触发实际的函数调用。中间步骤如所下。

# let plus5 = add(~x=5);
let plus5: (~y: int=?, unit) => int = <fun>;
# plus5(());
- : int = 5

两个中间步骤:

# let plus5 = add(~x=5);
let plus5: (~y: int=?, unit) => int = <fun>;
# let result8 = plus5(~y=3);
let result8: (unit) => int = <fun>;
# result8(());
- : int = 8

9.3 柯里化(进阶)

关于柯里化还可以看另一篇文章

柯里化(Currying)是偏函数应用对必选参数处理的一种具体应用。Currying函数意思是,将一个具有1个或多个参数的函数转换为一系列的一元函数的调用。 例如,二元函数add:

let add = (x, y) => x + y;

柯里化的意思是把它转换成以下函数:

let add = x => y => x + y;

现在我们需要像下面这样调用add:

# add(3)(1);
- : int = 4

我们得到了什么?偏函数应用变得容易了:

# let plus4 = add(4);
let plus4: (int) => int = <fun>;
# plus4(7);
- : int = 11

现在让人惊讶的是:ReasonML中所有的函数都是自动的柯里化的。这就是它如何支持偏函数应用的。你看柯里化后的add你就可以发现: (译者注:ReasonML来自于OCaml,它是一门古老的函数式编程语言)

# let add = x => y => x + y;
let add: (int, int) => int = <fun>;

换句话说:add(x, y)add(x)(y)相同,以下两种类型是等价的:

(int, int) => int
int => int => int

让我们用一个柯里化的二元函数来做一个总结。我们已知一个被柯里化的函数将会失去函数的意义,我们接下来要对拥有一个参数pair类型的函数进行柯里化。

let curry2 = (f: (('a, 'b)) => 'c) => x => y => f((x, y));
Let’s use curry2 with a unary version of add:
# let add = ((x, y)) => x + y;
let add: ((int, int)) => int = <fun>;
# curry2(add);
- : (int, int) => int = <fun>

最后的类型说明,我们已经成功地创建了一个二元函数。

10. 反向应用程序运算符(|>)

运算符|>被称为反向应用程序运算符或管道运算符。它让函数可以链式调用:x |> f与f(x)相同。这看起来不太相同,但是在组合函数调用时非常有用。

10.1 例子:管道输入整数和字符串

让我们从一个简单的例子开始。给定以下两个函数。

let times2 = (x: int) => x * 2;
let twice = (s: string) => s ++ s;

如果我们使用传统函数调用,我们会得到:

# twice(string_of_int(times2(4)));
- : string = "88"

首先,我们把4传给times2,然后将结果传给string_of_int(标准库中的一个函数)得到结果。 管道运算符让我们可以以下面的方式书写,并且更接近我们上面的描述: (译者注:这些都是函数式编程的思想)

let result = 4 |> times2 |> string_of_int |> twice;

10.2 例子:管道输入 lists

使用更复杂的数据和柯里式,我们得到了一种类似于面向对象编程中的链式调用的方法。 例如,下面的代码处理一个整数列表:

[4, 2, 1, 3, 5]
  |> List.map(x => x + 1)
  |> List.filter(x => x < 5)
  |> List.sort(compare);

这些功能将在后续的文章中详细解释。就目前而言,对它们的工作方式有一个大致的了解就足够了。 计算的三个步骤是:

# let l0 = [4, 2, 1, 3, 5];
let l0: list(int) = [4, 2, 1, 3, 5];
# let l1 = List.map(x => x + 1, l0);
let l1: list(int) = [5, 3, 2, 4, 6];
# let l2 = List.filter(x => x < 5, l1);
let l2: list(int) = [3, 2, 4];
# let l3 = List.sort(compare, l2);
let l3: list(int) = [2, 3, 4];

我们看到,在所有这些函数中,主参数都是最后传入。当我们使用管道时,首先通过偏函数应用传入次要参数,并创建一个函数。然后用管道运算符通过调用该函数传入主参数。 主参数与面向对象编程语言中的这个或self类似。

10.3 注意:一元函数的管道操作

当你使用偏函数应用为管道运算符创建操作数时,有一个容易犯的错误。看看你是否能在下面的代码中找出这个错误。

let conc = (x, y) => y ++ x;

"a"
  |> conc("b")
  |> conc("c")
  |> print_string();

/* Error: This expression has type unit
   but an expression was expected of type string */

问题:我们试图在某些地方使用零参数。这样运行会出错,因为print_string()print_string(())相同。并且print_string的单个参数是string类型(不是unit类型)。 如果在print_string之后省略了圆括号,那么一切正常:

"a"
  |> conc("b")
  |> conc("c")
  |> print_string;

/* Output: abc */

11. 设计函数类型签名的技巧

以下是设计函数类型签名的一些技巧:

  • 主要参数应是必选参数:
    • 如果一个函数只有一个主要参数,让它作为必选参数放在参数列表的最后。这样使函数可以支持管道运算符。
    • 有多个主要参数的函数也都是相似的。将这些主要参数作为必选参数放在最后。例如,一个函数将两个列表合并为一个列表。在这种情况下,两个list都应该是必选参数。
  • 例外:如果有两个或两个以上的不同类型的主要参数,那么应该给它们取别名。
    • 其它所有的参数都应该取别名。
    • 如果一个函数只有一个参数,它可以不需要取别名,即便它是严格的主要参数。

函数应该总是至少有一个必选参数。

  • 理由:
    • 一个必选参数,使你能够透明地添加可选参数,不用修改已有的函数调用,这将有助于api的开发。
    • 如果不需要必选参数,请使用():在解构时,我们把()作为一个必选参数作为匹配,但别使用这个值。
      let makePoint = (~x, ~y, ()) => (x, y);
      
    • 注意ReasonML任何时候都会自动添加给零元函数添加()作为参数。

这些规则背后的思想是使代码尽可能的可读:主要的(或唯一的)参数是由函数的名称来描述的,其余的参数由它们的别名来描述。 当一个函数有多个必选参数时,通常很难判断每个参数的作用。例如,比较以下两个函数调用。第二个更容易理解。

blit(bytes, 0, bytes, 10, 10);
blit(~src=bytes, ~src_pos=0, ~dst=bytes, ~dst_pos=10, ~len=10);

以上的的规则也应该适用于函数的名称。 ReasonML的命名风格使我想起了在Unix shell中调用命令。 本段摘自: “Suggestions for labeling” in the OCaml Manual.

12. 单参数匹配函数

ReasonML提供了一个可以让一元函数快速支持多参数的简单方法。以下面的函数为例。

let divTuple = (tuple) =>
  switch tuple {
  | (_, 0) => (-1)
  | (x, y) => x / y
  };

该函数的用法如下:

# divTuple((9, 3));
- : int = 3
# divTuple((9, 0));
- : int = -1

如果你使用fun运算符来定义divTuple,代码将变得更短:

let divTuple =
  fun
  | (_, 0) => (-1)
  | (x, y) => x / y;

13. (进阶)

接下来的所有内容都属于进阶内容。

14. 运算符

ReasonML的一个灵活的特性是运算符也是函数。如果你把它们放在括号里,你可以这样使用它们:

# (+)(7, 1);
- : int = 8

你还可以定义自己的运算符:

# let (+++) = (s, t) => s ++ " " ++ t;
let ( +++ ): (string, string) => string = <fun>;
# "hello" +++ "world";
- : string = "hello world"

通过将运算符放在括号中,你还可以轻松地查找它的定义类型:

# (++);
- : (string, string) => string = <fun>

14.1 运算符的规则

有两种运算符:二目运算符(在两个操作数之间)和单目运算符(在单个操作数之前)。 以下运算符两种运算都支持:

! $ % & * + - . / : < = > ? @ ^ | ~

二目运算符

First character Followed by operator characters
= @ ^ ❘ & + - * / $ % 0+
# 1+

另外,以下运算符也是二目运算符:

* + - -. == != < > || && mod land lor lxor lsl lsr asr

单目运算符:

First character Followed by operator characters
! 0+
? ~ 1+

此外, 另外,以下运算符也是单目运算符: - -. 本章节摘自: Sect. “Prefix and infix symbols” in the OCaml Manual.

14.2 操作符的优先级和结合顺序

下表列出了操作符及其结合顺序。一个操作符等级越高,它的优先级越高。例如,的优先级高于+。 Construction or operator | Construction or operator | Associativity | | ------------------------ | ------------- | | prefix operator | – | | . .( .[ .{ | – | | [ ] (array index) | – | | #··· | – | | applications, assert, lazy | left | | - -.(prefix) | – | | **··· lsl lsr asr | right | | ··· /··· %··· mod land lor lxor | left | | +··· -··· | left | | @··· ^··· | right | | =··· ··· ❘··· &··· $··· != | left | | && | right | | ❘❘ | right | | if | - | | let switch fun try | – | 说明:

  • op··· 意思是 op 后面跟着其它操作符.
  • Applications: function application, constructor application, tag application

本章节摘自: Sect. “Expressions” in the OCaml manual

14.3 什么时候关心结合顺序?

当运算符不可交换时,结合顺序就很重要。对于交换算子,操作数的顺序无关紧要。例如,+(+)是可交换的。然而,负(-)是不可交换的。 左结合意味着操作是从左向右运算。接下来的两个表达式是等价的: x op y op z (x op y) op z 减(-)操作是做结合的:

# 3 - 2 - 1;
- : int = 0

右结合性意味着操作是从右向左运算。接下来的两个表达式是等价的: x op y op z x op (y op z) 我们可以定义我们自己的右结合负算子。根据操作符表,如果它以@符号开始,它是自动右结合的: let (@-) = (x, y) => x - y; 如果我们这样使用,我们得到的结果与普通的减去的结果是不同的:

# 3 @- 2 @- 1;
- : int = 2

15. 多态函数

回想一下以前的文章中对多态的定义:对几种不同的类型进行相同的操作。多态性可以通过多种方式实现。OOP语言通过子类实现。重载是另一种流行的多态性。 ReasonML支持参数多态性:你可以使用(type variable)可变类型,而不是诸如int之类的具体类型作为参数类型或返回值类型。如果参数的类型是可变类型,那么任何类型的值都被接受。可变类型可以看作是函数类型的参数,因此称之为参数多态性。 使用可变类型的函数称为泛型函数。

15.1 例子: id()

例如,id是直接把参数作为返回值的标识函数:

# let id = x => x;
let id: ('a) => 'a = <fun>;

ReasonML的id类型很有趣:它不能检测x的类型,所以它使用可变类型'a来指示任何类型。所有名称以撇号开头的类型都是可变类型。ReasonML还推断,id的返回类型与它的参数类型相同。这有助于推断id的返回值类型。 换句话说:id是通用的,适用于任何类型。

# id(123);
- : int = 123
# id("abc");
- : string = "abc"

15.2 例子: first()

让我们来看另一个示例:first函数是一个用来获取pair类型第一个元件的通用函数。

# let first = ((x, y)) => x;
let first: (('a, 'b)) => 'a = <fun>;

first函数使用解构来访问该元组的第一个元件。类型推断机制告诉我们返回值得类型与第一个元件的类型相同。 我们可以使用一个下划线来表示我们对第二个组件不感兴趣:

# let first = ((x, _)) => x;
let first: (('a, 'b)) => 'a = <fun>;

如过加上参数类型声明,first如下:

# let first = ((x: 'a, _)) => x;
let first: (('a, 'b)) => 'a = <fun>;

15.3 例子:ListLabels.map()

先大致看一下,这个函数,后续的文章将详细说明。 ListLabels.map: (~f: ('a) => 'b, list('a)) => list('b)

15.4 重载VS参数多态性

注意,重载和参数多态性是不同的:

  • 重载为相同的操作提供了不同的实现。例如,一些编程语言允许你使用+进行算术、字符串连接或数组连接。
  • 参数多态性则是多种类型的单一实现。

16. ReasonML不支持不定参函数(可变参函数)

ReasonML不支持不定参函数。也就是说,你不能定义一个函数来计算任意数量的参数的和:

let sum = (x0, ···, xn) => x0 + ··· + xn;

相反,你必须为每个特性定义一个函数:

let sum2(a: int, b: int) = a + b;
let sum3(a: int, b: int, c: int) = a + b + c;
let sum4(a: int, b: int, c: int, d: int) = a + b + c + d;

你已经看到了与currying类似的技术,我们无法定义一个不定参的curry()函数,而必须使用二元函数curry2()来代替。你也会偶尔在一些库中看到。 这个技术的另一个变换是整数列表。

w3ctech微信

扫码关注w3ctech微信公众号

共收到0条回复