一. 使用lambda进行函数式编程

函数式代码是声明式的 - 你关注于做什么,并将如何做的细节分配给底层函数库。现在的Java这种命令式编程在去过很长时间都是主流,Java处理命令式风格还有面向对象风格,面向对象编程有助于抽象和封装;函数式编程的目标不是取代面向对象,真正需要关注的是命令式风格。kotlin和Java一样不仅提供了面向对象编程和命令式编程还有函数式编程,

Java和totlin都提供了lombda,下面看一下什么才是函数式!

1.1. 函数式风格

命令式:

fun doubleFunction() {
val list = mutableListOf<Int>()
for (i in 1..10) {
if (i % 2 == 0) {
list.add(i * 2)
}
}
println(list)
}

函数式

val list = (1..10)
.filter { i -> i % 2 == 0 }
.map { e -> e * 2 }

1.2. lambda表达式

lambda式短函数,用作高级函数的参数。我们可以使用lambda将一段可执行代码传递给函数,而不是将数据传递给函数。高阶函数可以依赖lambda来进行决策或者执行计算,而不是使用数据来进行决策或者执行计算。kotlin的lambda语法和Java的差不多:

{ perameter list -> body }

建议:将lambda作为参数传递给函数时,除非它是最后一个参数,否则不要急于创建多行lambda。在参数列表的中间有多行代码会使代码很难阅读,这就破坏了希望从lambda获取流畅性的好处。这种情况下,应该使用函数引用,而不是多行lambda。

下面举个例子:计算一个数字是否是质数?

fun isPrime(n: Int) = n > 1 && (2 until n).none { i -> n % i == 0 }

这里看一下none这个方法,这个方法是接受​​predicate: (T) -> Boolean​​一个lambda函数,这样也就大体知道为什么这样写了!

public inline fun <T> Iterable<T>.none(predicate: (T) -> Boolean): Boolean {
if (this is Collection && isEmpty()) return true
for (element in this) if (predicate(element)) return false
return true
}

这里有一个改进点,如果传递给函数的lambda只接受一个参数,就像​​none { i -> n % i == 0 }​​​这个一样,那么可以省略参数声明,而使用一个特殊的隐式名称​​it​​。

fun isPrime(n: Int) = n > 1 && (2 until n).none { n % it == 0 }

1.2.1. 接收lambda

接下来看一下高阶函数如何接受一个lambda。先看例子:

fun workTo(action: (Int) -> Unit, n: Int) = (1..n).forEach{ action(it) }

fun main() {
workTo({i -> print("$i, ") }, 10) // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
}

这里的action的类型是一个lambda表达式:​​action: (Int) -> Unit​​,规定了入参和出参。但是这个还有优化,一般来说会将lambda放在参数列表的最后:

fun workTo(n: Int, action: (Int) -> Unit) = (1..n).forEach{ action(it) }
// 调用
workTo(10, {i -> print("$i, ") })

但是这种调用方式并不是kotlin推荐的,下面看一下kotlin推荐我们写成下面这种。

workTo(10) { i -> print("$i, ") }

上面的例子说过传递给函数的lambda只接受一个参数,可以省略,最终写法是这样:

workTo(10) { print("$it, ") }

1.2.2. 使用函数引用

上面的例子lambda都是执行一些简单的操作,如果有一些复杂的操作,只能传递给函数进行处理:

({ x -> someMethod(x) })
// 简化写法
(::someMethod)

这里需要注意,如果传递给另一个lambda,那么我们不需要​​::​​。

这里其实没什么可说的,就是如果lambda是传递一个lambda,那么可以将其替换为另一个lambda的引用或者函数引用。例子:

fun workTo(n: Int, action: (Int) -> Unit) = (1..n).forEach(action)

fun main() {
workTo(10) { print("$it, ") } // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
workTo(10, ::print) // 12345678910
}

这里需要注意如果函数调用时单例上的,就和Java中的一样:

fun workTo(n: Int, action: (Int) -> Unit) = (1..n).forEach(action)

object Example {
fun sout(n: Int) = print(n)
}

fun main() {
workTo(10, Example::sout)
}

1.2.3. 函数返回函数

先看一个例子:

val names = listOf("Pam", "Pat", "Paul", "Paula")
println(names.find { name -> name.length == 5 })
println(names.find { name -> name.length == 3 })

上面的例子,可以看出虽然结果可以,但是代码冗余,​​name -> name.length == 5​​变化是有查找的长度,那么如何优化一下呢?

这里我们一定想是不是可以有一个接受length为参数,返回一个lambda的函数,让find方式去调用。看代码:

fun findLength(len: Int): (String) -> Boolean {
return { input: String -> input.length == len }
}
// 调用
println(names.find(findLength(5)))
println(names.find(findLength(3)))

这个findLength函数入参Int类型参数,返回的是一个函数签名,该函数接受字符串参数并返回布尔值作为输出。

这里findLength的返回类型,其实可以让kotlin自己推断,简化写法如下:

fun findLength(len: Int) = { input: String -> input.length == len }

1.3. 匿名函数

lambda通常作为参数传递给函数,但是如果在多个调用中需要相同的lambda,这可能会导致代码重复,这里有两种方法可以避免这种情况:

  • 将lambda存储到变量中以便重复使用
  • 创建匿名函数

先看第一种,我们将上面findLength修改为变量存储lambda:

val findLength = { name: String -> name.length == 5 }  // 第一种写法
val findLength: (String) -> Boolean = { name -> name.length == 5 } // 第二种写法
println(names.find { findLength(it) })

这里的例子举的不太好,对于现在来说其实将lambda存储到变量其实代码也是重复的,优化了寂寞,但是其他场景中还是很有用的。

对于其中的两种写法,就是kotlin的类型推断,第一种就是指明参数类型,让kotlin推断返回类型,第二种就是不需要推断,直接指明。

接着看第二种方式,匿名函数的使用,直接上列子:

val findLength = fun(name: String): Boolean { return name.length == 5 }
println(names.find { findLength(it) })
// 这样其实更好点
val findLength = fun(name: String, len: Int): Boolean { return name.length == len }
println(names.find { findLength(it, 5) })

这样可以实现上面的效果,但是需要注意如下匿名函数有一个return返回所有如果按照下面这样写是不对的:

names.find { fun(name: String): Boolean { return name.length == 5 } } // ERROR
names.find (fun(name: String): Boolean { return name.length == 5 }) // OK

错误是因为,如果​​find {}​​​是这样大括号里面需要的是一个执行结果,但是却传了一个函数所有类型不正确报错。而​​find()​​本身就是需要一个函数,这里lambda函数中的大括号和括号的区别还是需要注意下。

1.4. 闭包和词法作用域

先搞清楚什么是闭包?简单来说本来lambda是一个无状态的,输出取决于输入,但是有时需要依赖外部的值,导致lambda突破了定义范围,来绑定非布局的属性和方法,此时就是闭包。先看例子:

val doubleIt = { e: Int -> e * 2 } // 非闭包
/**********************************************/
val factor = 2
val doubleIt = { e: Int -> e * factor } // 闭包

这里还会引入一个概念:词法作用域。在闭包中​​e​​是参数,但是在主体内,变量或者属性factor不是局部的,编译器必须查看该变量闭包的定义范围(闭包的定义主体在哪里定义,如果没有找到,编译器将不得不在定义的范围内继续搜索,依次类推),这就是词法作用域。

这里也可以看一下上面的一个例子:

fun findLength(len: Int): (String) -> Boolean {
return { input: String -> input.length == len }
}

返回的lambda有一个input的参数,但是len不是lambda中的一部分,它来自闭包的词法作用域。

这里需要注意一点就是从闭包中读取或修改可变的局部变量,虽然kotlin不会报错,但是结果是不正确的。可变性在函数式编程中式禁忌!!例子:

var factor = 2
val doubled = listOf(1, 2).map { it * factor }
val doubledAlso = sequenceOf(1, 2).map { it * factor }

factor = 0
doubled.forEach { print("$it , ") } // 2 , 4 ,
println()
doubledAlso.forEach { print("$it , ") } // 0 , 0 ,

从上面看结果让人疑惑!所以在闭包中使用可变变量通常是错误的来源,应该尽量避免,保持闭包式纯函数,以避免混淆。

1.5. 非局部和带标签的return

默认情况下,return在lambda中是不允许存在的,即使它们返回一个值。这里会想到lambda和匿名函数之间一个显著的区别:如果有返回值,匿名函数必有return,并且它表示只从当前的lambda返回,而不是从外部调用函数返回。下面给一个例子看一下:

fun invokeWith(n: Int, action: (Int) -> Unit) {
println("enter invokeWith $n")
action(n)
println("exit invokeWith $n")
}

fun main() {
(1..3).forEach { i ->
invokeWith(i) {
println("enter for $i")
if (i == 2) {
return // 'return' is not allowed here
}
println("exit for $i")
}
}
}

虽然上面例子的代码是错误的,但是我们就是想退出,该怎么办呢?kotlin中可以使用带标签的return。改一下上面的例子:

(1..3).forEach { i ->
invokeWith(i) here@ {
println("enter for $i")
if (i == 2) {
return@here // 打标签
}
println("exit for $i")
}
}

这里的标签其实和Java的标签一样,带标签的return导致流程控制器跳转到带标签的块尾部,这样就跳出的lambda表达式,类似于​​continue​​,这就是显示标签的用法。

上面的标签return还可以使用隐式标签替代,如下:

(1..3).forEach { i ->
invokeWith(i) {
println("enter for $i")
if (i == 2) {
return@invokeWith
}
println("exit for $i")
}
}

kotlin中更推荐使用显示的标签,这样更有利于代码意图的表达。

接着在看一下另一个非局部的return,从上面的例子我们知道lambda不允许return,但是带标签的return是允许,先看例子:

(1..3).forEach { i ->
if (i == 2) {
return // @1
}
invokeWith(i) {
println("enter for $i")
if (i == 2) {
return@invokeWith // @2
}
println("exit for $i")
}
}

上面的代码是可以执行的,这里你肯定有疑问为什么​​@1​​位置的代码不需要代标签就可以直接返回呢?先看一下forEach的方法源码:

@kotlin.internal.HidesMembers
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}

猫腻在​​inline​​上面。下一小节再说。

1.6. 带有lambda的内联函数

lambda很优雅,但是有一个问题就是性能(由于lambda表达式都会被悄悄的编译成一个匿名类。这也就意味着需要占用内存。如果短时间内lambda表达式被多次调用,大量的对象实例化就会产生内存流失(Memory Churn)。)。kotlin提供了inline关键字来消除调用开销,从而提高性能、提供非局部控制流,

1.6.1. 内联优化

这里先看一下没有内联优化的情况,例子:

fun invoke(
n: Int,
action1: (Int) -> Unit,
action2: (Int) -> Unit
): (Int) -> Unit {
println("enter invoke $n")
action1(n)
action2(n)
println("exit invoke $n")
return { _: Int -> println("lambda returned from invoke") }
}

fun report(n: Int) {
println("")
print("called with $n")
val stackTrace = RuntimeException().stackTrace
println("stack depth: ${stackTrace.size}")
println("partial listing of the stack:")
stackTrace.take(3).forEach(::println)
}

fun main() {
/**
// 输出
enter invoke 1

called with 1stack depth: 6
partial listing of the stack:
com.example.one.ExampleKt.report(Example.kt:18)
com.example.one.ExampleKt$main$1.invoke(Example.kt:29)
com.example.one.ExampleKt$main$1.invoke(Example.kt:29)

called with 1stack depth: 6
partial listing of the stack:
com.example.one.ExampleKt.report(Example.kt:18)
com.example.one.ExampleKt$main$2.invoke(Example.kt:29)
com.example.one.ExampleKt$main$2.invoke(Example.kt:29)
exit invoke 1
*/
invoke(1, {i -> report(i) }, { i -> report(i) })
}

接着我们在invoke函数上添加inline,在看一下输出:

inline fun invoke(
n: Int,
action1: (Int) -> Unit,
action2: (Int) -> Unit
): (Int) -> Unit {
println("enter invoke $n")
action1(n)
action2(n)
println("exit invoke $n")
return { _: Int -> println("lambda returned from invoke") }
}

/**
enter invoke 1

called with 1stack depth: 3
partial listing of the stack:
com.example.one.ExampleKt.report(Example.kt:18)
com.example.one.ExampleKt.main(Example.kt:25)
com.example.one.ExampleKt.main(Example.kt)

called with 1stack depth: 3
partial listing of the stack:
com.example.one.ExampleKt.report(Example.kt:18)
com.example.one.ExampleKt.main(Example.kt:25)
com.example.one.ExampleKt.main(Example.kt)
exit invoke 1
*/

可以看到调用堆栈的前三层已经消失了,编译器对invoke函数扩展了字节码,编译器对两个lambda进行了内联,而不是直接调用,这种方式消除了调用开销。但是如果内联的函数非常大,并且在很多地方调用,那么生成的字节码可能比不使用online时还要大。

测试并优化,不要盲目的进行优化。

1.6.2. 参数不内联noinline

如果函数被标注为inline时,函数中所有的lambda参数将被内联,但是有时可能有的lambda不想内联呢?这时就需要使用kotlin提供的另一个关键字noinline消除该优化。例子:

inline fun invoke(
n: Int,
action1: (Int) -> Unit,
noinline action2: (Int) -> Unit
): (Int) -> Unit {
println("enter invoke $n")
action1(n)
action2(n)
println("exit invoke $n")
return { _: Int -> println("lambda returned from invoke") }
}
/**
enter invoke 1

called with 1stack depth: 3
partial listing of the stack:
com.example.one.ExampleKt.report(Example.kt:18)
com.example.one.ExampleKt.main(Example.kt:25)
com.example.one.ExampleKt.main(Example.kt)

called with 1stack depth: 5
partial listing of the stack:
com.example.one.ExampleKt.report(Example.kt:18)
com.example.one.ExampleKt$main$2.invoke(Example.kt:25)
com.example.one.ExampleKt$main$2.invoke(Example.kt:25)
exit invoke 1
*/

inline除了内联代码之外,还支持调用的lambda具有非局部return。上面的forEach已经看到了,下面在看一个例子:

invoke(1, {i -> 
if (i == 1) {
return // 此处 action已经被 inline
}
report(i)
}, { i ->
if (i == 2) {
return // ERROR 此处的action2被标注为noinline
}
report(i)
})

当使用inline时,不仅可以消除函数调用的开销,而且还可以获取对内联的lambda设置非局部return的能力。任何未内联的lambda都不能有非局部return

1.6.3. crossinline参数

如果一个函数被标记为inline,那么未标记为noinline的lambda参数将会自动认为是内联的。在函数调用lambda的地方,lambda主体将是内联的。此时会有一个问题,如果函数不调用给定的lambda,而是将lambda传递给另一个函数,或者传回给调用方呢?棘手的事,你不能内联未被调用的东西。

对于上面这种情况虽然使用noinline可以决解,但是如果你想让lambda在任何可能被调用的地方都内联呢?你可以要求内联请求传递给调用方,此时就可以使用crossinline。例子:

inline fun invoke(
n: Int,
action1: (Int) -> Unit,
action2: (Int) -> Unit
): (Int) -> Unit {
println("enter invoke $n")
action1(n)
action2(n)
println("exit invoke $n")
return { input: Int -> action2(input) } // ERROR Can't inline 'action2' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'action2'
}

此时是报错的,下面用crossinline修饰:

inline fun invoke(
n: Int,
action1: (Int) -> Unit,
crossinline action2: (Int) -> Unit
): (Int) -> Unit {
println("enter invoke $n")
action1(n)
action2(n)
println("exit invoke $n")
return { input: Int -> action2(input) }
}

到此,先总结下内联的知识点:

  • inline执行内联优化,来消除lambda调用的开销
  • crossinline也执行内联优化,不实在lambda所传递给的函数中,而是在最终调用它的任何地方
  • 只有传递给参数并且没有被标记为noinline或者crossinline的lambda才能有非局部return

1.7. inline和return总结

下面是关于inline和return的良好实践:

  • 不带标签的return总是来时函数的返回,而不是来自lambda的返回
  • 不带标签的return在非内联的lambda中是不允许的
  • 函数名是默认的标签,但不依赖于标签,如果选择使用带标签的return,请始终提供自定义的标签名称
  • 决定优化代码之前需要先测试性能,尤其是在优化lambda代码的时候,只有当你看到性能提高的时候才使用inline

二. 内部迭代和延迟计算

2.1. 外部/内部迭代器

先看一下什么是外部迭代器?例子:

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8)
for (i in numbers) {
if (i % 2 == 0 ) {
println(i)
}
}

下面用内部迭代器重写一下:

numbers.filter { it % 2 == 0 }.forEach { print("$it, ") }

2.2. 内部迭代器

2.2.1. filter/map/reduce

这三个可以和Java的lambda相类比,直接上例子:

data class Person(val firstName: String, val age: Int)
// 初始化数据
val people = listOf<Person>(
Person("Sara", 12),
Person("Jill", 51),
Person("Paula", 23),
Person("Mani", 25),
Person("Jack", 12),
Person("Sue", 70),
Person("Paul", 10),
)

需求:获取所有大于20岁人的名字,使用大写字母,用逗号分隔

// 这里的@2和@3步骤是可以合并的
val result = people.filter { it.age > 20 } // @1 过滤大于20岁的人
.map { it.firstName } // @2 选择firstName
.map { it.uppercase(Locale.getDefault()) } // @3 并将名字转大写
.reduce { names, name -> "$names, $name" } // @4 将其合并为一个按照逗号分隔的字符串
println(result) // JILL, PAULA, MANI, SUE

kotlin还提供了其他的操作,例如:sum/max。甚至join字符串等提供了很多专用的reduce函数,上面例子的reduce可以使用joinToString替换:

val result = people.filter { it.age > 20 }
.map { it.firstName.uppercase(Locale.getDefault()) }
.joinToString(",")
// 简写
val result = people.filter { it.age > 20 }
.joinToString(",") { it.firstName.uppercase(Locale.getDefault()) }

另一个例子,计算年龄总和:

// 方式一
val total1 = people.map { it.age }.reduce { total, age -> total + age }
// 方式二
val total2 = people.map { it.age }.sum()
// 方式三
val total3 = people.sumOf { it.age }

最后一个例子:获取最后一个或者获取第一个:

val first = people.filter { it.age > 50 }.map { it.firstName }.first()
val last = people.filter { it.age > 50 }.map { it.firstName }.last()

2.2.2. flatten/flatMap

此时如果有一个嵌套的列表,例如​​List<List<Person>>​​​如果想将其转换为​​List<Person>​​该如何操作?

val families = listOf(
listOf(Person("Sara", 12), Person("Paula", 51)),
listOf(Person("Jack", 12), Person("Paul", 51))
)

使用flatten将其展开:

val list = families.flatten()
println(list) // [Person(firstName=Sara, age=45), Person(firstName=Paula, age=31), Person(firstName=Jack, age=10), Person(firstName=Paul, age=61)]
println(list.size) // 4

当这种嵌套的结构可能是另一个结构通过map产生的如下:

val list = people.map { it.firstName }
.map(String::lowercase)
.map { name -> listOf(name, name.reversed()) }
/**
[
[sara, aras],
[jill, llij],
[paula, aluap],
[mani, inam],
[jack, kcaj],
[sue, eus],
[paul, luap]
]
*/

此时通过flatten可以搞定,如下:

val list = people.map { it.firstName }
.map(String::lowercase)
.map { name -> listOf(name, name.reversed()) }
.flatten()
// [sara, aras, jill, llij, paula, aluap, mani, inam, jack, kcaj, sue, eus, paul, luap]

但是还有一种方式也可以实现,就是通过flatMap,如下:

val list = people.map { it.firstName }
.map(String::lowercase)
.flatMap { name -> listOf(name, name.reversed()) }

// [sara, aras, jill, llij, paula, aluap, mani, inam, jack, kcaj, sue, eus, paul, luap]

上面两种方法效果是一样的,那么现在需要考虑下map和flatMap选择的问题:

  • 如果lambda是一个一对一的函数(也就是说,它接受一个对象或值,并返回一个对象或值),那么可以使用map转换原始集合
  • 如果lambda是一个一对多的函数(也就是说,它接受一个对象或值,并返回一个集合),那么可以使用map将原始集合转换为集合的集合
  • 如果lambda是一个一对多的函数,但是你希望将原始集合转换为经过转换的对象或值的集合,那么可以使用flatMap

2.2.4. 排序

处理遍历集合中的值外,还可以迭代过程中任何位置进行排序,你可以将其用作对函数管道的那个阶段可用的任何消息信息作为排序的标准。如下:

val result = people.filter { it.age > 20 }
.sortedBy { it.age }
.joinToString(",") { it.firstName }
// Paula,Mani,Jill,Sue

sortedBy默认升序排序,降序排序可以使用sortedByDescending。

2.2.5. 对象分组

通过函数管道转换数据的思想远远超过filter/map/reduce的基础。你可以根据不同的标准或属性对对象进行分组或放到bucket中。例子:

// 对名字的第一个字母进行分组, 返回的是一个map
val listMap = people.groupBy { it.firstName.first() }

这里其实还可以对value进行设置

// key是名字的第一个字符
// value是名字
val listMap = people.groupBy({it.firstName.first()}, {it.firstName})
// {S=[Sara, Sue], J=[Jill, Jack], P=[Paula, Paul], M=[Mani]}

看到这里就感觉kotlin真香,确实很多地方比Java的lambda使用起来简单点。

2.3. 延迟计算序列

上面看的例子都是关于集合的,集合是急切的,而序列是懒惰的。序列是对集合进行的优化的包装器,旨在提高性能。

2.3.1. 使用序列提高性能

这里我们还是使用2.2的例子,不过我们将放到filter和map的lambda表达式,换成函数引用,方便输出一些处理记录:

// 替代 filter { it.age > 20 }
fun isAdult(person: Person): Boolean {
println("isAdult called for ${person.firstName}")
return person.age > 20
}
// 替代 map { it.firstName }
fun fetchFirstName(person: Person): String {
println("fetchFirstName called for ${person.firstName}")
return person.firstName
}
// 执行
val result = people.filter(::isAdult).map(::fetchFirstName).first()
/**
isAdult called for Sara
isAdult called for Jill
isAdult called for Paula
isAdult called for Mani
isAdult called for Jack
isAdult called for Sue
isAdult called for Paul
fetchFirstName called for Jill
fetchFirstName called for Paula
fetchFirstName called for Mani
fetchFirstName called for Sue
*/

从上面的执行记录可以看到有好多输出,如果有大量的数据,大量的计算,其结果最终没有被使用,太浪费了!

那么针对上面的问题怎么优化呢?这时可以使用asSequence方法,直接看例子:

val result = people.asSequence().filter(::isAdult).map(::fetchFirstName).first()
/**
isAdult called for Sara
isAdult called for Jill
fetchFirstName called for Jill
*/

上面的例子只有在调用first方法时才会执行,本质上讲,序列将计算推迟知道调用了一个终端方法,然后最少的操作来获得期望的结果。但是数据量少的情况,使用和不使用序列效果时一样的,但是如果集合很大,那么使用序列将去除中间集合的巨大开销并消除计算。

2.3.2. 无限序列

序列除了会提高性能之外,还可以帮助执行按需计算,而反过来,又可以帮助创建无限或无穷的元素序列。

kotlin提供了好几种创建无限序列的方法,先介绍下generateSequence()函数,它是一个没有边界的序列,大小可以无限的生长,知道提供的元素的函数返回null为止,序列也就停止生成了。先看例子

generateSequence(2) { it * 2 }.take(5).toList() // [2, 4, 8, 16, 32]

其中的2的含义是说我们的序列从2开始然后根据传入的函数参数规则进行生成序列元素,我们的这里的规则就是上一个元素*2就是下一个元素的值。take()函数,传递一个Int的数值,告诉序列取到第5个的时候就停止了,序列也就不会再产生元素了。

在生成数据的过程中,也可以丢弃掉一些数据,可以使用drop抛弃掉一些数据,如下:

generateSequence(2) { it * 2 }.drop(2).take(5).toList() // [8, 16, 32, 64, 128]

接着看看一下另一个方法sequence,例子:

sequence {
var i = 1
while (true) {
yield(i)
i *= 2
}
}.drop(3).take(5).toList() // [8, 16, 32, 64, 128]

这样也可以实现上面的效果,其中yield会向调用方法返回一个值,然后在继续执行下一行代码。