Kotlin函数式编程(一) Kotlin与函数式编程

来源:互联网 发布:用友软件的优势 编辑:程序博客网 时间:2024/06/03 21:16

一、什么是Kotlin?

Kotlin是一门新兴的Jvm语言,与Scala等充满野心,想要取代Java的Jvm语言不同,Kotlin更像是Java的扩展,它能很好的和已有的Java代码一起工作,而比起Java,Kotlin提供了许多能够大幅提高开发效率的特性,使用Kotlin能写出比Java表现力更强,且更安全的代码。并且,Kotlin对函数式编程提供了比Java8更好的支持。
就在今年,Google宣布使用Kotlin作为安卓端的第二门官方编程语言,可以说,依托于巨大的安卓市场,Kotlin的前景是十分广阔的。

二、什么是函数式编程?

函数式编程是不同于过程式编程的另一种编程范式(现在流行的面向对象编程,实际上是过程式编程的一种编程思想)。函数式编程的思想在许多方面和过程式是冲突的,比如,过程式编程倾向于描述“怎么做”,而函数式编程则更倾向于描述“做什么”,过程式倾向于使用变量,而函数式则倾向于使用常量。尽管如此,函数式和过程式依旧是可以共存的。
相比过程式编程,函数式编程具有许多优势,比如:
· 代码更简洁
· 代码更容易推理
· 代码可复用性更强,API更灵活
· 函数式在编写多线程程序时更加容易
当然,相对的,函数式编程也具有一些缺点:
· 效率相对过程式较低
· 对于没有接触过函数式编程的程序员,函数式的代码就像文言文一样晦涩难懂
下面通过一个简单的例子:列表求和来比较一下过程式编程和函数式编程:
(实际上,Kotlin已经为数字列表扩展了sum方法,但为了方便演示,我们重新实现一边算法)

// 过程式代码:fun main(args: Array<String>) {    val ints = listOf(1, 2, 3, 4, 5)    var sum = 0    for (i in ints) sum += i    sum.log() // Log 15}

注释:
fun(function)关键字用于定义一个函数,在Kotlin中,函数可以定义在任意位置,因此main函数也不需要被包裹在类里面。
val(value)关键字定义一个不可变的变量,使用val关键字定义的变量不可以重新赋值。Kotlin具有类型推导的功能,如果变量的类型可以通过=右边的表达式推测出来,就可以省略变量的类型声明。
listOf函数用于构造一个不可变列表,不可变的数据结构不提供增删改的操作。
var(variable)关键字用于定义一个可变的变量。
for in循环遍历ints中的每一个元素,依次将元素赋值给i
.log()是一个自定义的扩展方法,用于将表达式的值呈现在控制台:

fun Any.log() = println(this)

我们再来看一段等效的函数式代码:

// 函数式代码:fun main(args: Array<String>) {    val ints = listOf(1, 2, 3, 4, 5)    ints.fold(0){ a, b -> a + b }.log()    // Log 15}

函数式的代码更加简洁一些,并且没有定义sum变量,而是使用fold方法对列表进行求和。fold方法对列表进行折叠,它接受两个参数,第一个参数是初始值,第二个参数是一个函数。这里我们使用了Lambda表达式,Lambda表达式是一个匿名函数,在这里,它等效于:

// 函数体只有一条语句时,包裹函数体的花括号可以简化成等号fun plus(a: Int, b: Int): Int = a + b

像fold这样接受另一个函数作为参数,或是返回一个函数的函数,我们称之为“高阶函数”,在Kotlin中,如果要传入一个Lambda表达式给一个函数的最后一个参数,那么该Lambda表达式可以被放置在函数调用的小括号外侧。
fold方法遍历整个列表,不断将列表中的元素与初始值“结合”,而结合的方法就是我们提供的Lambda,即将两个整数相加:
这里写图片描述
从图中可以看出,fold采用的算法其实和过程式直接用循环是一样的。
从这个简单的例子中,我们大概能看出一丝函数式编程的风格,但仍然没有体现出函数式编程的优势。我们再通过一个更加复杂的例子来观察一下:

// 过程式代码:fun main(args: Array<String>) {    val list = listOf(            listOf(1, 2, 3),            listOf(4, 5, 6),            listOf(7, 8, 9)    )    // 求出每一个子列表的乘积,再求和    var sum = 0    for (sub in list) {        var product = 1        for (i in sub) product *= i        sum += product    }    sum.log() // Log 630}

似乎代码变得复杂了起来,我们必须读完整个循环才能明白程序的意图。那么函数式如何呢:

// 函数式代码:fun main(args: Array<String>) {    val list = listOf(            listOf(1, 2, 3),            listOf(4, 5, 6),            listOf(7, 8, 9)    )    list.map { it.fold(1){ a, b -> a * b } }            .fold(0){ a, b -> a + b }            .log() // log 630}

这里调用了map方法。map方法将函数应用到列表的每一个元素上,从而转化整个列表。再来观察一下这个Lambda表达式:

{ it.fold(1){ a, b -> a * b } }

在Kotlin中,如果一个Lambda表达式只有一个参数,那么可以使用it关键字来指代这个参数,从而省略参数列表。在这里,it指代的是list中的子列表。我们对每一个子列表都采用乘法进行折叠。由于折叠之后,整个列表被折成了一个整数,所以在调用map之后,list从一个嵌套的二维列表变成了一个一维的列表。
第二个fold则和第一个例子中对整数求和的fold方法一模一样。
这么写的好处是显而易见的:在过程式的代码中,我们再阅读循环时,必须追踪其中的每一个变量(sum, product, sub 和 i)才能弄明白程序的意图,而函数式的代码则不依赖任何外部变量和上下文,每条语句要表达的计算已经完全包含在这条语句中了。
你可能会说,过程式代码容易debug,比如,我们想要查看每一个子表达式的乘积,只需要条件一条语句:

...    for (sub in list) {        var product = 1        for (i in sub) product *= i        product.log()        sum += product    }...

事实上,函数式的代码同样可以实现,并且不需要破坏现有的调用链:

...    list.map { it.fold(1){ a, b -> a * b } }            .apply { log() }            .fold(0){ a, b -> a + b }            .log() // log 630

apply方法比较特殊,obj.apply{ log() }相当于调用了:

fun <T> f(obj: T): T {    obj.log()    return obj}

它可以在不破坏调用链的情况下调用一个对象上的返回值为Unit的方法。