Scala 函数式特征

1. 函数类型

函数类型是函数式语言的特征之一;

其原因在于,函数是语言中的一等公民,可以作为变量,而变量是具有类型的。

Scala 的函数类型定义如下:

1
f: Int, Int => Int

使用箭头将参数类型和返回值类型相间隔;

上面的例子表示函数 f 接受两个 Int 参数,返回值类型为 Int

2. 高阶函数

高阶函数指的是接受 函数作为参数 的函数,它的参数是函数类型。

Scala 中的高阶函数如下:

1
2
3
def sum(f: Int => Int, a: Int, b: Int) =
if(a > b) 0
else f(a) + sum(a + 1, b)

其中,f 是函数类型的参数,它接受一个 Int 作为参数,返回值是一个 Int

上面的例子如下数学公式的求法:

abf(a)\sum_a^b{f(a)}

3. 匿名函数(函数字面量, lambda)

作为语言的基本类型,如字符串,我们可以使用字面量表示它,如:

1
2
val s = "abc"
println(s)

上面可以直接写成

1
println("abc")

在 Scala 中,函数也具有这种特性,我们可以直接定义一个函数字面量:

1
2
val f = (x: Int) => x * x
sum(f, 1, 3)

如上,f 是一个函数,具有参数 x,返回 x 的平方

也可以将字面量直接传入

1
sum(x => x * x, 1, 3)

大部分情况都不需要显式指定参数的类型,编译器会进行自动推断;
同时,无法在函数字面量中显式指定函数的返回值类型
只能通过定义函数变量的类型来进行显示指定

实际上,Scala 中的匿名函数就是其他语言中的 lambda 表达式;

就函数式上来说,Scala 提供了一种更轻便的语法

4. 柯里化

4.1 定义

柯里化是函数式范式的一个特有现象;

它指的是,一个函数,通过接受部分参数,可以返回接受剩余参数的 嵌套函数

事实上,对于一个函数

def f(arg1)(argn)=Edef \ f(arg_1)\ldots(arg_n) = E

n>1n \gt 1 时,以下的写法和上面是等价的:

def f(arg1)(argn)={def g(argn)=E; g}def \ f(arg_1)\ldots(arg_n) = \{def \ g(arg_n) = E; \ g\}

所以,我们可以通过编写嵌套的接受部分参数的函数,并返回它,来达到柯里化的目的;

实际上,这个过程就叫做柯里化。

\begin{align} f(arg_1)(arg_2)\ldots(arg_n) \\ &= arg_1 \Rightarrow \{f(arg_2)\ldots(arg_n)\} \\ &= arg_1 \Rightarrow \{arg_2 \Rightarrow \{f(arg_3)\ldots(arg_n)\}\} \\ &= \cdots \\ &= arg_1 \Rightarrow arg_2 \Rightarrow arg_3 \Rightarrow \ldots \Rightarrow f \end{align}

4.2 显式柯里化

sum 函数可以使用如下的方法进行重写:

1
2
3
4
5
6
7
def sum(f: Int => Int): (Int, Int) => Int = {
def sumF(a: Int, b: Int) = {
if(a > b) 0
else f(a) + sumF(a + 1, b)
}
sumF
}

上面的写法被称作 显式柯里化,就是将一个接受多个参数的函数通过显式编写一个内部的嵌套函数,并返回这个函数来达到柯里化。

在调用时,我们可以直接如下调用:

1
sum(x => x * x) (1, 10) // 1^2 + 2^2 + ... + 10^2

第一个括号,调用了外部函数,返回值是内部的 sumF 函数;

这使得我们可以 继续使用括号 进行 sumF 的调用

4.3 隐式柯里化

许多函数式编程语言都提供柯里化的语法糖,这被称作 隐式柯里化

Scala 也提供了这样的语法糖:

1
2
3
def sum(f: Int => Int)(a: Int, b: Int) =
if (a > b) 0
else f(a) + sum(f)(a + 1, b)

通过使用两个括号,就可以直接定义最内部的函数体,而不需要再定义一个内部的嵌套函数;

这可以让我们像进行柯里化函数调用一样,定义柯里化函数

4.4 柯里化的目的

柯里化相比我们定义一个多参数函数来说,要稍显复杂;

那么为什么不直接定义一个多参数函数呢?

实际上,使用柯里化的目的在于可以动态确定参数;

当函数的某些参数不确定时,我们可以先保存一个存根;

剩余的参数确定之后,可以通过存根直接调用剩下的参数。

柯里化的另一个用处类似建造者模式(Builder Pattern),可以通过柯里化来减少参数和函数重载的爆炸。

5. 部分应用(partially application)

部分应用指的是, 固定 函数的某些参数,可以获取一个接受剩下参数的函数;

有点类似于在运行时给予函数默认值。

Scala 的部分应用写法如下:

1
2
3
4
5
def add(a: Int, b: Int, c: Int) = a + b + c

def addA5 = add(5, _:Int, _:Int)

addA5(2, 3) // 5 + 2 + 3

可以看到,我们通过将参数 a 的值固定为 5 得到了一个新的函数;

它接受 bc,返回 5 + b + c

6. 柯里化和部分应用的区别

这两个概念经常被混淆,但是实际上有着一些差别:

  1. 柯里化指的是将多参数函数 分解为 多个单参数(组)函数的特性
  2. 部分应用指的是通过 固定 某个参数,得到接受剩余参数函数的特性

虽然它们调用的效果都是返回一个函数,但是,两者一次调用返回的函数具有显著的不同:

  1. 柯里化返回的函数只接受一个参数(组)

    由于返回的是层层嵌套的函数,所以会出现函数的连续调用
    add(1)(1)(1)(1)(1) 中,
    对于一个 (1),返回的函数是接受另一个 1,同时将剩下的内部嵌套闭包返回

  2. 部分应用返回的函数可以接受多个参数

    相比柯里化,部分应用返回的函数可以直接接受多个参数,如
    add_1(1,1,1,1)
    固定了第一个 1 之后,剩下的 1 可以直接传入,而不需要连续调用

柯里化通过将函数分解嵌套来减少函数的参数;

函数的部分应用通过给予参数默认值来减少函数的参数。

柯里化函数的调用是函数的连续调用,而函数的部分应用是函数的一次调用。

0%