函数式编程

命令式编程,其实就是最常见的编程方式:在编程的时候,我们需要告诉计算机每一步具体都要干什么。

1
2
3
4
5
6
7
8
9
10
11
fun foo(): List<Int> {
val list = listOf(1, 2, 3, 4)
val result = mutableListOf<Int>()
for (i in list) {
if (i % 2 == 0) {
result.add(i)
}
}

return result
}

函数式,或者说“声明式”的代码

1
fun fp() = listOf(1, 2, 3, 4).filter { it % 2 == 0 }

区别:

  • 它只需要声明我们想要什么,而不必关心底层如何实现。
  • 代码更加简洁,可读性更高。

3.0 版本的词频统计程序,其实并没有完全发挥出 Kotlin 函数式编程的优势,因为其中的“getWordCount()”“mapToList()”都是我们自己实现的。事实上,我们完全可以借助 Kotlin 标准库函数来实现。

1
2
3
4
5
6
7
8
9
fun processText(text: String): List<WordFreq> {
return text
.clean()
.split(" ")
.filter { it != "" }
.groupBy { it }
.map { WordFreq(it.key, it.value.size) }
.sortedByDescending { it.frequency }
}

到底什么是函数式编程?

函数是一等公民

  • 函数可以独立于类之外,这就是 Kotlin 的顶层函数;
  • 函数可以作为参数和返回值,这就是高阶函数和 Lambda;
  • 函数可以像变量一样,这就是函数的引用;
  • 当函数的功能更加强大以后,我们就可以几乎可以做到:只使用函数来解决所有编程的问题。

    纯函数

  • 函数不应该有副作用。所谓副作用,就是“对函数作用域以外的数据进行修改”,而这就引出了函数式的不变性。在函数式编程当中,我们不应该修改任何变量,当我们需要修改变量的时候,我们要创建一份新的拷贝再做修改,然后再使用它(这里,你是不是马上就想到了数据类的 copy 方法呢?)。
  • 无副作用的函数,它具有幂等性,换句话说就是:函数调用一次和调用 N 次,它们的效果是等价的。
  • 无副作用的函数,它具有引用透明的特性。
  • 无副作用的函数,它具有无状态的特性。

实战:函数式的循环

for 循环,是命令式编程当中最典型的语句

1
2
3
4
5
6
7
8
fun loop(): Int {
var result = 0
for (i in 1..10) {
result += i
}

return result
}

仅仅只使用函数,我们该如何实现这样的功能呢?答案其实很简单,那就是递归。

1
2
3
4
5
6
fun recursionLoop(): Int {
fun go(i: Int, sum: Int): Int =
if (i > 10) sum else go(i + 1, sum + i)

return go(1, 0)
}

我们知道,递归都是有调用栈开销的,所以我们应该尽量使用尾递归。对于这种类型的递归,在经过栈复用优化以后,它的开销就可以忽略不计了,我们可以认为它的空间复杂度是 O(1)。

1
2
3
4
5
6
7
8
fun recursionLoop(): Int {
// 变化在这里
// ↓
tailrec fun go(i: Int, sum: Int): Int =
if (i > 10) sum else go(i + 1, sum + i)

return go(1, 0)
}

在实际的开发工作中,这种方式是不推荐的,毕竟它太绕了,对吧?如果要在工作中实现类似的需求,我们使用 Kotlin 集合操作符一行代码就能搞定:

1
fun reduce() = (1..10).reduce { acc, i -> acc + i } // 结果 55

Kotlin 还为我们提供了另一个更简单的操作符,也就是 sum:

1
fun sum() = (1..10).sum() // 结果 55

表达式思维

不变性思维

空安全思维

关注我