Kotlin语言之函数式编程

Kotlin语言是大名鼎鼎的JetBrains公司(就是可以甩Eclipse数条大街的IntelliJ IDEA背后的公司)出品的现代的编程语言,之前已经在IDEA中蹦达出来很多次了;只是最近随着Google在其2017年的I/O大会上将其列为Android平台官方支持的语言而窜上了热点。

本文尝试从函数式编程的角度管窥Kotlin的特性。

JVM上的函数式语言生态

作为一门比较年轻的编程语言,要想在既有的数百种语言中脱颖而出,成功吸引开发者的心,对新的函数式编程范式的支持是必然不可少的 - 这一点基本成为语言出品商心照不宣的潜规则了,当然在21实际,不支持面向对象的范式也是说不过去的。

作为基于JVM平台的语言,和Java的互操作性肯定是一个重要的优势,当然这方面已经有成熟的函数式语言scala和更早一点的clojure在前。可能比较遗憾的是,正统的函数式编程风格太难被传统的OO程序员所接受,因此基于传统Lisp的clojure一直曲高和寡,scala在近年来有变得更加流行的趋势,只是目前看来仍然没有跨越期望的引爆点

有丰富的特性还希望有速度

传统印象中的静态函数式语言的编译速度往往会比较慢,这一点在工程实践上是个很重要的因素。

Kotlin作为后来者,其开发者认为静态语言的编译速度是个至关重要的,然后Scala的编译速度远不能令人满意。对大型的项目而言,笨拙的编译速度浪费的可是大量的时间和金钱;毕竟天下武功唯快不破,更快的编译时间意味着更快的反馈周期,更多次的迭代开发。Kotlin的目标之一是期望编译速度可以像Java一样快,benchmark分析也表明了二者的速度是差别不大的。

基本特性

函数式语言的基本元素就是function,这一点kotlin倒是没有玩太多花头。用fun关键字来声明函数,函数是第一等公民,可以支持函数作为参数,返回函数等基本特性。

不可变类型支持

Kotlin强制要求程序员声明某个特定的变量是否是可变类型。

如果是可变类型,则需要用var来声明;那么后续程序中任何地方访问变量都会被IDE给highlight出来,提醒可能的副作用。因为可变类型意味着内部存储着状态,从函数式编程的角度来看,状态会影响函数的纯度,带来副作用和复杂性。

immutable_hints.png

函数声明

基本的函数声明是这样的

fun thisIsAFunction(x: Int) : Int {
}

当然这里的类型后置语法和传统的C家族语言有些不同,但是适应起来倒也不是难事儿。

类型推导

Kotlin也支持强大的类型推导,从而在很多情况下,可以省略不必言的类型指定,简化代码;譬如函数的返回类型可以被自动推断的时候,其类型声明可以被省略。

特殊的返回类型 Unit

Unit是一个特殊的类型,用于指定某个函数返回的值可以被省略,类似于Java8的Void类型。如果一个函数没有返回值,那么可以指定其返回Unit或者直接省略其返回

fun someFunc(arg: SomeType) : Unit {
    // do something with arg
    // no return needed
}

// same as above
fun someFunc(arg: SomeType) {
    // do something
}

中缀表达式

中缀表达式写法更替进人的思维习惯,在定义某些操作符的时候是非常有用的。此用法往往用于扩展已有类型的操作,定义的时候需要满足以下条件

  • 属于某个类的成员函数,或者是定义某个类的扩展函数(后边再回头来看),因为这里我们必须知道左侧的操作对象是谁
  • 必须只有一个函数参数(操作符后边的对象)
  • infix关键字来标记

譬如

infix fun Int.shl(x : Int) -> Int {
  /// implementation of shl operation
}

// call site
1 shl 2

命名参数和默认值

这点和Python很像在多个参数的复杂函数的使用上有很大帮助,能极大提高可读性减少维护成本。调用方可以在调用点指定需要传入的参数的名字;也可以省略掉不需要指定的参数。

譬如有如下的reformat函数用于格式化

reformat(str,
    normalizeCase = true,
    upperCaseFirstLetter = true,
    divideByCamelHumps = false,
    wordSeparator = '_'
)

调用点可以简单写作

reformat(str, wordSeparator = '_')
// equals to
reformat(str, true, true, false, '_')

这个功能在传统的C++/Java里边没有提供,但是IDEA提供了只能提示可以弥补Java的不足;而Kotlin则将其内置在语言中了;本身没多少复杂性在里边。

高阶函数和语法糖

高阶函数

函数的参数可以是一个函数,这个在Kotlin的库里已经有大量的例子,譬如基本的Sequence的filter函数携带一个谓词函数,其针对给定的参数返回一个Boolean

public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
    return FilteringSequence(this, true, predicate)
}

单参数函数的表达式形式

当函数只有一行实现的时候,可以省略其函数体,直接用=来书写,就像复制给一个变量一样

fun add2Numbers(x : Int, y: Int): Int = x+y

Lambda和匿名函数

匿名函数用大括号括起来,上面的例子也可以写作

val add2Numbers2 = {x : Int, y: Int -> x+y}

函数调用的形式省略

当函数仅仅有一个参数的时候,其参数名字默认为it保留关键字可以不用显示指定。

当函数的最后一个参数是一个函数的时候,其函数体可以用{}块的方式来书写,获得更好的可读性。

譬如如下的例子用于打印指定数目个偶数

val printEvens = { x: Long ->
    IntStream.range(1, 10000000)
            .filter { it%2 == 0 }.limit(x)
            .forEach { println(it) }
}

一个具体一点的例子

假设要实现如下功能的函数

  1. 遍历某个目录树
  2. 找出所有符合条件的文件夹
  3. 取其文件绝对路径
  4. 归并为一个字符串列表返回

可以通过如下几个函数完成

fun extractAllDomainDoc(dirName: String) {
    File(dirName).walkTopDown().filter { isDocDir(it) }
            .map { it.absolutePath }.toList()
}

private fun isDocDir(file: File): Boolean {
    return file.isDirectory && isDomainDocDir(file)
}

private fun isDomainDocDir(file: File): Boolean {
    return file.absolutePath.split(File.separator)[file.absolutePath.split(File.separator).size - 1] == "doc"
}

这里每个函数的含义都是比较清楚易懂的。如果利用上述的省略规则,那么可以更简略的写为

fun extractAllDomainDoc(dirName: String) = File(dirName).walkTopDown()
        .filter { isDocDir(it) }
        .map { it.absolutePath }.toList()

private fun isDocDir(file: File) = file.isDirectory && isDomainDocDir(file)

private fun isDomainDocDir(file: File) = file.absolutePath
        .split(File.separator)[file.absolutePath.split(File.separator).size - 1] == "doc"

类型扩展函数

Kotlin 支持对已有的类型添加扩展,值需要在任何想要的地方添加想要的功能,则原有的类型即可像被增强了一样具有新的功能,该机制提供了OO之外新的灵活的扩展方式。

譬如默认的Kotlin的Iterable类没有提供并发的foreach操作,可以通过扩展机制很容易的写出来一个使用ExecutorService来并发循环的版本

// parallel for each, see also https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections
fun <T, R> Iterable<T>.parallelForEach(
        numThreads: Int = Runtime.getRuntime().availableProcessors(),
        exec: ExecutorService = Executors.newFixedThreadPool(numThreads),
        transform: (T) -> R): Unit {

    // default size is just an inlined version of kotlin.collections.collectionSizeOrDefault
    val defaultSize = if (this is Collection<*>) this.size else 10
    val destination = Collections.synchronizedList(ArrayList<R>(defaultSize))

    for (item in this) {
        exec.submit { destination.add(transform(item)) }
    }

    exec.shutdown()
    exec.awaitTermination(1, TimeUnit.DAYS)
}

这里在函数体中,this自动会绑定于被扩展的对象。

如果我们想实现一个自动将一大堆plantuml文件转换为png格式并copy到指定目录,因为默认的plantuml的API是单线程的,我们可以基于上述的parallelForEach实现来并发调度UML的生成过程,对应的代码可以写为

markDownFileLists.parallelForEach {
    SourceFileReader(File(it)).generatedImages.firstOrNull()?.apply {
        copyFileToDirWith(this.pngFile.absolutePath, getCopyTarget)
        println("${System.currentTimeMillis()} - Created png for $it")
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容