Scala入门,以Java程序员的视角(二):first-class函数

《Scala学习手册》中文版将first-class翻译成首类,在这里不过多纠结这个翻译。实际上first-class表明了函数在函数式编程中的地位,它属于第一公民!

函数类型、函数值

在Scala里,函数本身也可以当作实例传入传出,那么可以用如下形式表示函数的类型:([<type>, ...]) => <type>

在def的函数标识符double(即函数名)赋给val变量时,需要有显示的类型,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala> def double(x: Int): Int = x * 2
double: (x: Int)Int
scala> val myDouble = double
<console>:12: error: missing argument list for method double
Unapplied methods are only converted to functions when a function type is expected.
You can make this conversion explicit by writing `double _` or `double(_)` instead of `double`.
val myDouble = double
^
scala> val myDouble: Int => Int = double
myDouble: Int => Int = <function1>
scala>

没有显示的类型会报错,在REPL不能单独输入double这个函数的标识符;

等中间的数字表明参数列表的大小(即多少个参数);

如果不想有显示的类型,可以使用通配符_

1
2
3
4
scala> val myDouble = double _
myDouble: Int => Int = <function1>
scala>

函数字面量(匿名函数)

一个函数可以让某个函数类型的值作为输入参数或返回值,那么这个函数就是高阶函数;

传递函数给高阶函数,可以采用函数字面量内联定义;

这里的函数字面量对应的是Java 8中的Lambda表达式;

接下来看看函数字面量的表示形式:

1
2
3
4
scala> val doubler = (x: Int) => x * 2
doubler: Int => Int = <function1>
scala>

可以看到一个匿名函数,他有输入(即(x: Int)),有返回(即x * 2),但是就是没有函数名;

在上一篇文章中,我说“=”相当于Java 8的“->”,那是根据“=”之于后面出现的大括号的作用而言的,在这里“=>”也相当于Java 8中的“->”,这也是根据“=>”对前后承接的内容产生的作用而言的;

下面看看函数字面量对函数值的修改:

1
2
3
4
scala> val maximize = (a: Int, b: Int) => if (a > b) a else b
maximize: (Int, Int) => Int = <function2>
scala>

可以看出,此时给maximize赋值不需要添加显示类型

占位符语法

使用条件书中列了两条:

  • 外置位已经指定了类型:只有这样,当你使用_,函数能匹配出其类型;
  • 参数不重复使用:重复使用你就不知道哪个_代表哪个参数了;

这里用一个简单的例子说明:

1
2
3
4
5
6
7
8
9
10
scala> def add(a: Int, b: Int, f: (Int, Int) => Int) = f(a, b)
add: (a: Int, b: Int, f: (Int, Int) => Int)Int
scala> add(3, 5, _ * _)
res52: Int = 15
scala> add(3, 5, (a: Int,b: Int) => a * b)
res53: Int = 15
scala>

可见,占位符能对匿名函数进行很好的缩写;

部分应用函数和柯里化

这一小节主要讲用单参数函数“解构”多参数函数,柯里化可以使获得部分应用函数更加简洁;

普通方式

1
2
3
4
5
6
7
8
9
10
11
12
scala> def factorOf01(x: Int): Int => Boolean = {
| def temp(y: Int): Boolean = {
| y % x == 0
| }
| temp
| }
factorOf01: (x: Int)Int => Boolean
scala> factorOf01(4)(4)
res7: Boolean = true
scala>

普通方式是用内嵌函数实现的,外围函数接收一个内嵌函数的返回;

通配符赋值的方式

1
2
3
4
5
6
7
8
9
scala> def factorOf(x: Int, y: Int) = y % x == 0
factorOf: (x: Int, y: Int)Boolean
scala> val test = factorOf(3, _)
<console>:12: error: missing parameter type for expanded function ((x$1) => factorOf(3, x$1))
val test = factorOf(3, _)
^
scala>

此时需要显示指定类型;

更正后:

1
2
3
4
5
6
7
scala> val test = factorOf(3, _: Int)
test: Int => Boolean = <function1>
scala> test(4)
res2: Boolean = false
scala>

用通配符_替代参数,当继续调用函数时,不需要再输入3;

柯里化方式

柯里化方式就是第一篇末尾提到的参数组:

1
2
3
4
5
6
7
8
9
10
scala> def factorOf03(x: Int)(y: Int) = y % x == 0
factorOf03: (x: Int)(y: Int)Boolean
scala> val test03 = factorOf03(4) _
test03: Int => Boolean = <function1>
scala> test03(4)
res8: Boolean = true
scala>

可以发现柯里化比起前面两种更清晰和简洁;

传名参数

传名参数类似Java 8里的行为参数化,将函数作为参数传入时并不进行计算,而在方法体内,每调用一次执行一次。接下来会首先采用传名参数的方法来使用函数调用,之后会采用类似Java 8中Lambda表达式实现的方式给出另一个效果相同的版本;

传名参数的方法

首先定义被传入参数的函数doubles:

1
2
3
4
5
6
7
scala> def doubles(x: => Int) = {
| println("Now doubling " + x)
| x * 2
| }
doubles: (x: => Int)Int
scala>

现在它接受一个Int值,也接受一个返回Int值的函数,在定义返回Int值的函数f:

1
2
3
4
scala> def f(i: Int) = { println(s"Hello from f($i)"); i}
f: (i: Int)Int
scala>

测试输出结果:

1
2
3
4
5
6
7
8
9
10
11
scala> doubles(4)
Now doubling 4
res48: Int = 8
scala> doubles(f(8))
Hello from f(8)
Now doubling 8
Hello from f(8)
res49: Int = 16
scala>

f(8)的计算延迟到方法体中进行,每次调用都会执行一次f(8);

类似Java 8中的方法

在Scala中不采用传名参数也是可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
scala> def doubles(x: Int => Int, y: Int) = {
| println("Now doubling " + x(y))
| x(y) * 2
| }
doubles: (x: Int => Int, y: Int)Int
scala> doubles(f, 8)
Hello from f(8)
Now doubling 8
Hello from f(8)
res50: Int = 16
scala>

可见打印出的结果和之前采用传名参数的方法相同;

在Java 8中,doubles函数传入的x参数通常是Lambda表达式,而这里直接传入函数值;

这样做的好处其实在《Java 8实战》中有提到,比如打印日志,不必每次都执行而是视条件是否满足而定,这样会在一些场景下节约不必要的开销;

偏函数

有的资料中中文翻译的偏函数部分应用函数彼此是颠倒的,这里还是按照本书中文翻译为准;

只能部分应用于输入数据的函数称为偏函数,即对有些传入的参数,函数将不能工作;

用函数字面量块调用高阶函数

实际在上一篇已经讨论过一些(){},这里先放上一些实例;

实例1:

1
2
3
4
5
6
7
8
9
10
11
12
scala> def ni01(f: String => String) = {
| f("nihao")
| }
ni01: (f: String => String)String
scala> ni01({ s => "Hello"})
res10: String = Hello
scala> ni01{ s => "Hello"}
res11: String = Hello
scala>

从这个例子可以看出,Scala的匿名函数是可以通过{}包裹后传入函数的,(){}那么()可以省略;

实例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scala> def ni02(s: String)(f: String => String) = {
| f(s)
| }
ni02: (s: String)(f: String => String)String
scala> ni02("Hello 02"){s => s}
res12: String = Hello 02
scala> ni02("Hello 02"){ s => { println(s)
| s
| }
| }
Hello 02
res17: String = Hello 02
scala>

从这个例子可以看出,参数组在遇到{}同样可以省略()

实例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scala> def aca[A](f: => A) = f
aca: [A](f: => A)A
scala> def ff(i: Int) = i
ff: (i: Int)Int
scala> aca(ff(4))
res9: Int = 4
scala> val bbb = aca { print("hello")
| 3
| }
hellobbb: Int = 3
scala>

可以看到aca是传名函数,参数可以是函数值、函数的调用以及{}块,这里{}虽然可以复合语句,但是其实是将其当作传入的函数值来运用,值是最后的的表达式或者语句返回的内容;

小结

本篇算是上一篇的进阶,重要的知识点包括函数类型、匿名函数、传名函数啊等等,通过研究这些知识点,本人对Scala函数有了更深入的理解。