Scala学习笔记 之 神奇的函数式编程(FP)

函数是一等公民

var定义变量
val定义常量
推荐多使用val

函数是一个表达式

1
def square(a: Int) = a * a

使用Block表达式时默认最后一行的返回是返回值

1
2
3
def squareWithBlock(a: Int) = {
a * a
}

函数可以赋值给val或var,也可以作为参数传递给另一个函数

1
def addOne(f: Int => Int, arg: Int) = f(arg) + 1

当表达式没有返回值时,默认返回Unit

借贷模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import scala.reflect.io.File
import java.util.Scanner

def withScanner(f: File, op: Scanner => Unit) = {
val scanner = new Scanner(f.bufferedReader)
try {
op(scanner)
} finally {
scanner.close()
}
}

withScanner(File("/proc/self/stat"),
scanner => println("pid is " + scanner.next()))

鸭子类型

走起来像鸭子,叫起来像鸭子,就是鸭子。

使用{ def close() : Unit }作为参数类型,任何定义了close函数的类都可以作为参数,类似于Java里面的接口,但是更加灵活,不必使用继承这种不够灵活的特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def withClose(closeAble: { def close(): Unit },
op: { def close(): Unit } => Unit) {
try {
op(closeAble)
} finally {
closeAble.close()
}
}

class Connection {
def close() = println("close Connection")
}

val conn: Connection = new Connection()
withClose(conn, conn =>
println("do something with Connection"))

部分应用(Partial application)

可以使用下划线”_”部分应用一个函数,得到另一个函数。下划线可以被看作一个神奇的通配符。

1
2
3
def adder(m1: Int, m2:Int, m3:Int) = m1 + m2 + m3
val add2 = adder(_:Int, 2, _:Int);
add2(1, 3) // 运行结果为6

adder _adder(_, _, _)的简写,即取函数的全部参数作为部分应用。
在作为参数时,这种写法可以再度简化,如:

1
list.foreach(println _)

可以进一步简化为

1
list.foreach(println)

然而不能直接将def的函数赋值到一个val上,需要用部分应用取得一个值函数,如:

1
2
val add3 = adder // 编译器报错
val add3 = adder _ // 正常运行,返回一个值函数

柯里化(Currying)

1
def add(x:Int, y:Int) = x + y

是普通的函数。

1
def add(x:Int) = (y:Int) => x + y

是柯里化后的函数,相当于返回一个匿名函数表达式。

1
def add(x:Int)(y:Int) = x + y

是简化后的写法

在柯里化的基础上使用部分应用:

1
2
scala> val plusOne = add(1)_
onePlus: (Int) => Int = <function1>

以下是柯里化的应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def withClose(closeAble: { def close(): Unit })
(op: { def close(): Unit } => Unit) {
try {
op(closeAble)
} finally {
closeAble.close()
}
}

class Connection {
def close() = println("close Connection")
}

val conn: Connection = new Connection()
withClose(conn)(conn =>
println("do something with Connection"))

重复参数(Repeated parameters)

Scala允许在定义函数时,指定函数的最后一个参数是可重复的。如下:

1
def echo(args: String*) = args.foreach(println)

以上的代码中,String*表示参数args可以是0到多个,也就是说可以有以下的调用:

1
2
3
4
5
6
scala> echo()
scala> echo("one")
one
scala> echo("one", "two")
one
two

在函数体的内部,参数argsArray[String]类型的,所以可以在其上调用foreach方法。然而如果在传参时传入一个Array[String]类型的数据则会报错,如下:

1
2
3
4
5
6
scala> val arr = Array("What's", "up", "doc?")
arr: Array[java.lang.String] = Array(What's, up, doc?)
scala> echo(arr)
<console>:7: error: type mismatch;
found : Array[java.lang.String]
required: String

为使其能够如预期的工作,可以用如下的语法:

1
2
3
4
scala> echo(arr: _*)
What's
up
doc?

注意冒号与下划线间需要有空格。这个标记告诉编译器将arr的每一项作为一个参数传递给函数echo,而不是整个作为一个参数传递给echo.

指定参数名称

对于如下的函数定义:

1
scala> def speed(distance: Float, time: Float): Float =  distance / time

在调用函数时,可以指定参数的名称,从而可以按自定义的顺序传参:

1
2
3
4
scala> speed(distance = 100, time = 10)
res29: Float = 10.0
scala> speed(time = 10, distance = 100)
res30: Float = 10.0

这在函数有默认参数时尤其有用:

1
2
3
def printTime2(out: java.io.PrintStream = Console.out,
divisor: Int = 1) =
out.println("time = "+ System.currentTimeMillis()/divisor)

对于以上的函数,可以像这样调用

1
2
printTime2(out = Console.err)
printTime2(divisor = 1000)

按名称传递参数

按名称传递参数时,参数会等到实际使用的时候才被计算

1
2
3
4
5
6
7
8
val logEnable = false

def log(msg: String) =
if (logEnable) println(msg)

val MSG = "programing is running"

log(MSG + 1 / 0)

以上的代码中log函数按值传递参数,所以程序运行会产生异常。

1
def log(msg: String)

改为

1
def log(msg: => String) //:和=>之间需要有空格!

(上面的语法表示按名称传递参数)
再运行上面的代码将不会产生异常,因为参数在运行时没有被用到,所以参数的计算过程从未发生过。

以上的代码实际上是下面代码的缩写:

1
def log(msg: () => String) = if (logEnable) println(msg())

也就是说传递的参数类型实际上是一个返回值为String的函数,这样就更容易理解了。当省略掉括号时,msg的调用也省掉了参数,于是看起来就像是一个String类型的参数一样:

1
def log(msg: => String) = if (logEnable) println(msg)

尾部递归函数的优化

有时候为实现某些功能,我们可以写出递归的代码,看下面的代码:

1
2
3
def approximate(guess: Double): Double =
if (isGoodEnough(guess)) guess
else approximate(improve(guess))

上面的函数就是一个递归函数,事实上,它也正好是一个尾部递归函数。对于一个尾部递归函数,我们可以将其改写为一个基于while循环的非递归函数,如下:

1
2
3
4
5
def approximate(initGuess: Double): Double = {
var guess = initGuess
while (!isGoodEnough(guess)) guess = imporve(guess)
guess
}

上面的函数定义与其递归版本的定义实现了完全一样的功能。
而这两个版本的函数各有其优缺点:递归版本的函数避免了使用var变量,且代码优雅,清晰,易读;然而非递归的版本由于没有大量的函数调用,相比递归版本将有更好的效率。

在Scala中,我们不用去纠结这个问题了,因为编译器会自动将尾部递归的函数定义转化为基于while循环的非递归函数,如此,我们既可以写出优雅易读的代码,又可以不用担心其效率问题了!所以我们在写Scala代码时,可以尽情的写尾部递归的函数定义,因为这样的函数定义思路清晰,优雅易读!