《Kotlin核心编程》 ——2.3.8 “柯里化”风格、扩展函数

举报
华章计算机 发表于 2020/02/21 20:51:49 2020/02/21
【摘要】 本节书摘来自华章计算机《Kotlin核心编程》 —— 书中第2章,第2.3.8节,作者是水滴技术团队 。

2.3.8 “柯里化”风格、扩展函数

我们已经知道,函数参数化是一种十分强大的特性,结合Lambda表达式,能在很大程度上提高语言的抽象和表达能力。接下来,我们再来了解下高阶函数在Kotlin中另一方面的表现,即一个函数返回另一个函数作为结果。

通过之前的介绍,相信你已经能很容易地理解什么是返回一个函数。还记得我们上面使用过的例子吗?

fun foo(x: Int) = { y: Int -> x + y }

表达上非常简洁,其实它也可以等价于:

fun foo(x: Int): (Int) -> Int {

    return { y: Int -> x + y }

}

现在有了函数类型信息之后,可以很清晰地发现,执行foo函数之后,会返回另一个类型为(Int) -> Int的函数。

如果你看过一些介绍函数式编程的文章,可能听说过一种叫作“柯里化(Currying)”的语法,其实它就是函数作为返回值的一种典型的应用。

简单来说,柯里化指的是把接收多个参数的函数变换成一系列仅接收单一参数函数的过程,在返回最终结果值之前,前面的函数依次接收单个参数,然后返回下一个新的函数。

拿我们最熟悉的加法举例子,以下是多参数的版本:

fun sum(x: Int, y: Int, z: Int) = x + y + z

sum(1, 2, 3)

如果我们把它按照柯里化的思路重新设计,那么最终就可以实现链式调用:

fun sum(x: Int) = { y: Int ->

    { z: Int -> x + y + z }

}

sum(1)(2)(3)

你会发现,柯里化非常类似“击鼓传花”的游戏,游戏开始时有个暗号,第1个人将暗号进行演绎,紧接着第2个人演绎,依次类推,经过一系列加工之后,最后一个人揭晓谜底。在这个过程中:

开始的暗号就是第1个参数;

下个环节的演绎就是返回的函数;

谜底就是柯里化后最终执行获得的结果。

可见柯里化是一个比较容易理解的概念,那么为什么会有柯里化呢?

柯里化与Lambda演算

我们说过,Lambda演算是函数式语言的理论基础。在严格遵守这套理论的设计中,所有的函数都只能接收最多一个参数。为了解决多参数的问题,Haskell Curry引入了柯里化这种方法。值得一提的是,这种技术也是根据他的名字来命名的—Currying,后续其他语言也以此来称呼它。

说到底,柯里化是为了简化Lambda演算理论中函数接收多参数而出现的,它简化了理论,将多元函数变成了一元。然而,在实际工程中,Kotlin等语言并不存在这种问题,因为它们的函数都可以接收多个参数进行计算。那么,这是否意味着柯里化对我们而言,仅仅只有理论上的研究价值呢?虽然柯里化在工程中并没有大规模的应用,然而在某些情况下确实起到了某种奇效。

在我们之前介绍过的Lambda表达式中,还存在一种特殊的语法。如果一个函数只有一个参数,且该参数为函数类型,那么在调用该函数时,外面的括号就可以省略,就像这样子:

fun omitParentheses(block: () -> Unit) {

    block()

}

omitParentheses {

    println("parentheses is omitted")

}

此外,如果参数不止一个,且最后一个参数为函数类型时,就可以采用类似柯里化风格的调用:

fun curryingLike(content: String, block: (String) -> Unit) {

    block(content)

}

curryingLike("looks like currying style") {

    content ->

    println(content)

}

// 运行结果

looks like currying style

它等价于以下的的调用方式:

curryingLike("looks like currying style", {

    content ->

    println(content)

})

实际上,在Scala中我们就是通过柯里化的技术,实现以上简化语法的表达效果的。Kotlin则直接在语言层面提供了这种语法糖,这个确实非常方便。然而需要注意的是,通过柯里化实现的方案,我们还可以分步调用参数,返回值是一个新的函数。

curryingLike("looks like currying style")

// 运行报错

No value passed for parameter 'block'

然而,以上实现的curryingLike函数并不支持这样做,因为它终究只是Kotlin中的一种语法糖而已。它在函数调用形式上近似柯里化的效果,但却并不是柯里化。Kotlin这样设计的目的是让我们采用最直观熟悉的套路,来替代柯里化实现这种简洁的语法。

Scala中的corresponds方法是另一个典型的柯里化应用,用它可以比较两个序列是否在某个比对条件下相同。现在我们依靠Kotlin上面这种特殊的类柯里化语法特性,再来实现一个Kotlin版本。

首先,我们先穿插下Kotlin的另一项新特性—扩展函数,这是Kotlin中十分强大的功能,我们会在第7章中重点介绍。当前我们先简单了解下它的使用,因为corresponds方法需要借助它来实现。

简单来说,Kotlin中的扩展函数允许我们在不修改已有类的前提下,给它增加新的方法。如代码清单2-4所示。

代码清单 2-4

image.png

在这个例子中,类型View被称为接收者类型,this对应的是这个类型所创建的接收者对象。this可以被省略,就像这样子:

fun View.invisible() {

    visibility = View.INVISIBLE

}

我们给Android中的View类定义了一个invisible方法,之后View对象就可以直接调用该方法来隐藏视图。

views.forEach { it.invisible() }

回到我们的corresponds方法,基于扩展函数的语法,我们就可以对Array<T>类型增加这个方法。由于Kotlin的特殊语法支持,我们还是采用了定义普通多参数函数的形式。

fun <A, B> Array<A>.corresponds(that: Array<B>, p: (A, B) -> Boolean): Boolean {

    val i = this.iterator()

    val j = that.iterator()

    while (i.hasNext() && j.hasNext()) {

        if (!p(i.next(), j.next())) {

            return false

        }

    }

    return !i.hasNext() && !j.hasNext()

}

然后再用柯里化的风格进行调用,就显得非常直观:

val a = arrayOf(1, 2, 3)

val b = arrayOf(2, 3, 4)

 

a.corresponds(b) { x, y -> x+1 == y } // true

a.corresponds(b) { x, y -> x+2 == y } // false

虽然本节讲述的是函数作为返回值的应用,然而由于Kotlin的特殊语法,我们可以在大部分场景下用它来替代柯里化的方案,显得更加方便。


【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。