柯里化 (Currying) 及其应用

柯里化(Currying,以逻辑学家 Haskell Brooks Curry 的名字命名)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数作为参数的函数。乍一看不容易理解,我们结合几个例子来看看,在 scala 中,currying 是什么样的,以及什么时候我们可以考虑使用它。

样例一:加法

从最简单的开始,我们看一下 currying 是怎么把一个接受两个参数的函数改造成每次只接受一个参数的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// uncurry
def add1(x: Int, y: Int): Int = {
x + y
}

// shorthand
def add2(x: Int)(y: Int): Int = {
x + y
}
add2(2) _
add2(2)(3)

// longhand
def add3(x: Int): (Int => Int) = {
(y: Int) => {
x + y
}
}
add3(2)
add3(2)(5)

输出结果:

1
2
3
4
res0: Int => Int = $$Lambda$1120/855429058@4702faee
res1: Int = 5
res2: Int => Int = $$Lambda$1119/1771691170@f6b85c3
res3: Int = 7

从这里,我们可以看出,实际上 add2(2) 已经变成了一个 method,而 add3(2) 则变成了一个 functionmethodfunction 的差别参考我之前的博客),它们都可以将任何接受的参数加上 2 得到输出的结果。

样例二:foldLeft

上面的例子实际上已经很清楚的说明了 currying 的特点,即固定一个参数作为函数的一部分,然后接受剩下的参数进行处理。下面这个例子是 scala 内置的代码 foldLeft,我们平时也经常会用到,它将对一个列表的操作转换成了依次对列表中每个元素的操作:

1
2
3
4
5
6
7
8
9
10
def foldLeft[B](z: B)(op: (B, A) => B): B = {
var acc = z
var these = this
while (!these.isEmpty) {
acc = op(acc, these.head)
these = these.tail
}
acc
}
List(1,2,3).foldLeft("res:")((x: String, y:Int) => x + y)

输出结果:

1
res:123

样例三:人群分析报告

最后,我们再举一个例子,尝试着说明一下在什么情况下可以考虑使用 currying。考虑我们有一个函数能够接受用户的各种属性和待分析的视频 ID,输出具备指定属性的用户对该视频的详细分析报告(好希望有),它的非 currying 形式如下:

1
def godView(age: String, gender: String, city: String, netType: String, videoID: String): Unit = { /** whoCanWhoUp **/ }

每天业务都会根据数据分析去定向 push 一些视频,但是有些 push 的效果不好,就需要分析为啥给某群用户推荐这个视频不好,我们借助这个 godView 函数来进行分析。假设我们有几个人被分到了不同的人群和一些视频列表,每个人都得仔细根据函数的输出的报告进行分析。比如我分到了使用 wifi 的中年上海男性,那我每次分析都需要把这些参数全都传进去:

1
2
3
4
godView("35-45", "male", "shanghai", "wifi", "video1")
godView("35-45", "male", "shanghai", "wifi", "video2")
godView("35-45", "male", "shanghai", "wifi", "video3")
...

对于我来说,多希望不需要每次传这些相同的参数,让我专心分析视频 ID 就好了,那我就希望把这个函数改造一下:

1
def godView(age: String)(gender: String)(city: String)(netType: String)(videoID: String): Unit = { /** whoCanWhoUp **/ }

然后我只把我关注的人群提取出来,再进行分析,就简洁多了:

1
2
3
4
5
val myView = godView("35-45")("male")("shanghai")("wifi") _
myView("video1")
myView("video2")
myView("video3")
...

Take-aways

从以上三个例子中,我们可以窥见 currying 的几个特点:

  1. Currying 是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术;
  2. Currying 其实只是复用代码的一种思路,它并非不可替代;
  3. 当你发现你要调用一个函数,并且调用参数都是一样的情况下,这个参数就可以进行 currying,以便更好的完成任务;
  4. Currying 允许你写出来的代码更干净、更有表达力。