原文:原文:http://2ality.com/2017/12/functions-reasonml.html
翻译:ppp
系列文章目录详见: “什么是ReasonML?”
本文详解ReasonML中的函数。
匿名函数定义如下:
(x) => x + 1;
这个函数有一个参数 x
,函数体为x+1
。
你可以通过把这个匿名函数用let绑定(赋值)给一个变量的方式来给这个函数命名。
let plus1 = (x) => x + 1;
然后你就可以这样调用这个函数了:
# plus1(5);
- : int = 6
函数同时也可以作为另一个函数的参数。为了演示这个特性,我们将简单的使用一下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()
是初阶函数。
函数的体是一个表达式。我们已知块也是表达式,所以对plus1
来说的以下两个定义是等价的。
let plus1 = (x) => x + 1;
let plus1 = (x) => {
x + 1
};
如果函数只有一个参数,且参数为一个合法的标识符,那么可以省略括号。
let plus1 = x => x + 1;
ReasonML的编译器和编辑插件会对未使用的变量发出警告。例如:下面的函数就出现 “unused variable y” 的警告。
let getFirstParameter = (x, y) => x;
你可以通过给没有用到的参数加一个下划线的方式来避免这个警告。(译者注:这在定义接口时出 于对今后扩展的考虑常用到)
let getFirstParameter = (x, _y) => x;
你也可以直接用 _ 作为变量名,多次使用都是允许的:
let foo = (_, _) => 123;
通常,你只能访问已经通过let绑定即已经存在的值。这意味着你不能通过let来定义互递归函数或自递归函数。
让我们先解释一下互递归函数(参考)。下面的两个函数 even
和 odd
就是互递归函数。你必须使用专用的关键字"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
的表达式结束。
even
和 odd
就这样基于对方的定义而定义。
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
你同样需要使用let rec
来定义自递归,因为函数体里当出现递归调用时,函数的定义都还没完成。例如:
let rec factorial = (x) =>
if (x <= 0) {
1
} else {
x * factorial(x - 1)
};
factorial(3); /* 6 */
factorial(4); /* 24 */
函数的arity性质是它的参数的个数。例如,factorial()
的arity是1。下面是参数个数从0到3的函数对应的名称:
除了3,我们讨论了4元,5元函数,等等。如果函数的arity是可以变化的,叫做可变参函数。
函数是我们接触到的第一种复杂类型:通过组合其它类型而构建的类型。让我们使用ReasonML的命令行rtop来确定两个函数的类型。
首先,定义一个函数add()
:
# let add = (x, y) => x + y;
let add: (int, int) => int = <fun>;
所以add的类型是:
(int, int) => int
箭头表明了add
是一个函数。它有两个参数,并且有一个int
类型的返回值。
(int, int) => int
的这种标记方法也被称作为add
函数的类型签名。它描述了函数输入输出的类型。
然后,定义一个高阶函数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
在一些静态类型的编程语言中,你必须为函数的所有参数和函数的返回值提供类型声明。例如:
# 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
运算符+
来运算,从而推断出x
和y
是int
类型:
# let add = (x, y) => x + y;
let add: (int, int) => int = <fun>;
换句话说:大部分类型的声明都是可选的。
尽管类型声明是可选的,但提供它们有时也会使类型推断更有效。例如,下面的函数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>;
我喜欢函数的编码风格是对所有参数都类型都作出声明,但是让ReasonML推测返回值得类型。除了能让类型检查更有效外,对参数类型的声明也是对代码的一种注释(调用时自动检查参数的一致性)。
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没有零元函数,它只是在定义和调用时把默认参数()
隐藏了。
为什么ReasonML没有零元函数?这是归根于ReasonML对函数的处理是通过偏函数应用(partial application)方式进行的(稍后将详细解释)。其导致的结果是:调用时如果不提供完整的参数,那么将生成并返回一个新函数,这个函数的参数是调用时没有传入的那些剩余的参数。因此,如果你不传入参数,那么func()
的返回值将与func
相同,也是一个函数。也就是说,执行func()
将不会做任何事情。
在任何对变量进行绑定的时候都存在解构:存在于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>;
到目前为止,我们只接触过位置参数:在调用时由实参的位置决定它将被绑定到(赋值给)哪个与它对应的形参上。
但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
关于参数别名有一个问题需要提醒:由于参数别名可以不受位置约束,这类函数类型只有在参数别名按相同顺序声明时才会成功匹配。 看看以下三个函数。
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
在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
做什么?x
和y
通过=?
被声明为可选参数。因此,两者都是可选类型(int)
。可选类型本身是一种变体,详情将在即将发表的文章中解释。现在,我将简要介绍一下。可选类型的定义为:
type option('a) = None | Some('a);
当调用add()
时,将用到可选类型:
~x
,那么x
将被绑定为None
。~x
的值,那么x将被绑定为(123)。换句话说,可选类型是把值封装了起来,而示例中的switch
又拆解了它们。(译者注:短短几句说不清,看后续文章吧)
为什么在add
函数最后要添加unit
类型的参数(也可以说是一个空参数)?
let add = (~x=?, ~y=?, ()) =>
···
原因与偏函数应用(partial application)有关(稍后将详细解释)。简而言之,这里有两件事是矛盾的:
为了解决这个冲突,当遇到第一个必选参数时,ReasonML会为所有缺失的可选参数赋上默认值。但在遇到必选参数之前,ReasonML仍然会按照可选参数处理。也就是说,你总是需要一个必选参数来触发这一操作。由于add()
没有必选参数,所以我们添加了一个空。在对函数参数解构时,()
模式将会强制把必选参数用()
赋值。
添加空参数的另一个原因是,我们将无法触发两个默认值,因为add()
与add(())
相同。
这种稍微有点奇怪的方法的优点是,你可以获得偏函数应用和可选参数两者的好处。
当你为可选参数声明类型时,你必须使用option(···):
let add = (~x: option(int)=?, ~y: option(int)=?, ()) =>
···
add函数的类型是:
(~x: int=?, ~y: int=?, unit) => int
不幸的是,在这种情况下,函数定义与类型不同。理由是我们需要区分这两样东西:
在下一节中,我们将使用参数的默认值,这样内部类型就不同了,但是外部类型(也因此add函数的类型)却是相同的。
处理缺失的参数是个麻烦事:
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
函数的用法和之前完全相同:我们把是如何处理缺失函数这件事,通过默认值的方式隐藏在了函数里,对使用者透明。
如果有默认值,那么对参数类型的声明如下:
let add = (~x: int=0, ~y: int=0, ()) => x + y;
add 的类型为:
(~x: int=?, ~y: int=?, unit) => int
在内部,可选参数是作为可选类型的元素被接收的(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
的默认值。
偏函数应用是一种使函数更加通用的机制:如果在函数调用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
偏函数应用允许你编写更紧凑的代码。为了演示如何使用,我们将使用一个List:
# let numbers = [11, 2, 8];
let numbers: list(int) = [11, 2, 8];
接下来,我们将使用标准库函数List.map
。List.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]
哪个版本比较好?这取决于你的喜好。第一个版本可以说是逻辑描述更清晰,第二个版本则更简洁。
偏函数应用更大的亮点是与管道运算符(|>
)一起使用,用于函数的组合(稍后将对此进行解释)。
到目前为止,我们见过带必选参数的偏函数应用,它同样也可以使用参数的别名。在来看看带参数别名版本的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
关于柯里化还可以看另一篇文章
柯里化(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>
最后的类型说明,我们已经成功地创建了一个二元函数。
运算符|>被称为反向应用程序运算符或管道运算符。它让函数可以链式调用:x |> f与f(x)相同。这看起来不太相同,但是在组合函数调用时非常有用。
让我们从一个简单的例子开始。给定以下两个函数。
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;
使用更复杂的数据和柯里式,我们得到了一种类似于面向对象编程中的链式调用的方法。 例如,下面的代码处理一个整数列表:
[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类似。
当你使用偏函数应用为管道运算符创建操作数时,有一个容易犯的错误。看看你是否能在下面的代码中找出这个错误。
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 */
以下是设计函数类型签名的一些技巧:
函数应该总是至少有一个必选参数。
let makePoint = (~x, ~y, ()) => (x, y);
这些规则背后的思想是使代码尽可能的可读:主要的(或唯一的)参数是由函数的名称来描述的,其余的参数由它们的别名来描述。 当一个函数有多个必选参数时,通常很难判断每个参数的作用。例如,比较以下两个函数调用。第二个更容易理解。
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.
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;
接下来的所有内容都属于进阶内容。
ReasonML的一个灵活的特性是运算符也是函数。如果你把它们放在括号里,你可以这样使用它们:
# (+)(7, 1);
- : int = 8
你还可以定义自己的运算符:
# let (+++) = (s, t) => s ++ " " ++ t;
let ( +++ ): (string, string) => string = <fun>;
# "hello" +++ "world";
- : string = "hello world"
通过将运算符放在括号中,你还可以轻松地查找它的定义类型:
# (++);
- : (string, string) => string = <fun>
有两种运算符:二目运算符(在两个操作数之间)和单目运算符(在单个操作数之前)。 以下运算符两种运算都支持:
! $ % & * + - . / : < = > ? @ ^ | ~
二目运算符
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.
下表列出了操作符及其结合顺序。一个操作符等级越高,它的优先级越高。例如,的优先级高于+。 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 | – | 说明:
本章节摘自: Sect. “Expressions” in the OCaml manual
当运算符不可交换时,结合顺序就很重要。对于交换算子,操作数的顺序无关紧要。例如,+(+)是可交换的。然而,负(-)是不可交换的。 左结合意味着操作是从左向右运算。接下来的两个表达式是等价的: 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
回想一下以前的文章中对多态的定义:对几种不同的类型进行相同的操作。多态性可以通过多种方式实现。OOP语言通过子类实现。重载是另一种流行的多态性。 ReasonML支持参数多态性:你可以使用(type variable)可变类型,而不是诸如int之类的具体类型作为参数类型或返回值类型。如果参数的类型是可变类型,那么任何类型的值都被接受。可变类型可以看作是函数类型的参数,因此称之为参数多态性。 使用可变类型的函数称为泛型函数。
例如,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"
让我们来看另一个示例: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>;
先大致看一下,这个函数,后续的文章将详细说明。 ListLabels.map: (~f: ('a) => 'b, list('a)) => list('b)
注意,重载和参数多态性是不同的:
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微信公众号
共收到0条回复