Kotlin与函数式编程

Kotlin 简介

由俄罗斯圣彼得堡的JetBrains团队开发的新编程语言,其名称来自于圣彼得堡以西约30公里处的科特林岛。
Kotlin是一种运行在Java虚拟机上的静态类型编程语言。它可以被编译成Java字节码、JavaScript代码、本地机器码。支持与Java,Android 100% 完全互操作。

发展历史
  • 2011年7月,JetBrains推出Kotlin项目

  • 2012年2月,JetBrains以Apache 2许可证开源此项目

  • 2016年2月,发布Kotlin v1.0,这被认为是第一个官方稳定版本

  • 2017年5月,在Google I/O上,Google宣布在Android上提供最佳支持(转正)

  • 2018年8月,最新稳定版 Kotlin 1.2.60发布

函数式编程

函数式编程中最古老的例子莫过于1958年被创造出来的Lisp了。Lisp由约翰·麦卡锡(John McCarthy,1927-2011)在1958年基于λ演算所创造,采用抽象数据列表与递归作符号演算来。较现代的例子包括Haskell、Erlang等。现代的编程语言对函数式编程都做了不同程度的支持,例如:JavaScript, Coffee Script,PHP,Perl,Python, Ruby, C# , Java 等

那函数式编程该如何理解?

我们先来看一个例子,现有一个字符串集合,除去集合中的单字符项,且字符串的首字母要大写
最后返回一个用逗号分隔组成的字符串。

过程式编程如下

fun cleanNames(listOfNames: List<String>):String{   
     val result = StringBuilder()     
   for (i in listOfNames.indices) {       
     if (listOfNames[i].length > 1) {
        result.append(capitalizeString(listOfNames[i])).append(",")}  
      }      
  return result.substring(0, result.length - 1).toString()
}  

 fun capitalizeString(s: String): String {  
      return s.substring(0, 1).toUpperCase() + s.substring(1, s.length)   
 }

过程式编程会考虑使用循环。每迭代一个名字,我们都检查它的长度是否大于一个字符的保留门槛,然后调整其首字母为大写后,连同作为分隔符的逗号一起,追加到result。最后一个名字不应该有尾随的逗号,所以我们从最后的返回值里去掉了这个多余的分隔符。

过程式编程将操作安排在循环内部去执行。本例中我做了三件事:

  1. filter,筛选列表,去除单字符条目;
  2. transform,变换列表,使名字的首字母变成大写;
  3. 接着是convert,转换列表,得到单个字符串。

可以看出我们主要是对列表进行遍历迭代后进行了三步操作。而函数式语言为这些操作提供了针对性的辅助手段。

函数式编程语言对如上一小节所列的几种操作(filter、transform、convert),每一种都作为一个逻辑分类由不同的函数所代表,这些函数实现了低层次的变换,但依赖于开发者定义的函数类型对象(高阶函数)作为参数来调整其低层次运转机构的运作。

函数式编程实现如下

   fun cleanNames(names: List<String>?): String {

    // Kotlin 功能

fun cleanNames(names: List<String>?): String {

    return names?.filter { name -> name.length > 1 }?.
                  map { name -> capitalize(name) }?.
                  reduce { acc, s -> "$acc,$s" } ?: ""

}

fun capitalize(e: String): String {
    return e.substring(0, 1).toUpperCase() + e.substring(1, e.length)
}

看完上面两个例子,我们对函数式编程应该有了印象,总结一下就是:

函数式编程是一种”编程范式”,它的主要思想是把运算过程尽量写成一系列的函数调用。使用不可变值和函数,函数对一个值进行处理,映射成另一个值。

流行的面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。

那函数式编程我们已经了解了它的思想,接下来看看它有什么特点

特点

函数式编程具有五个鲜明的特点。

  1. 函数是”第一等公民”

所谓”第一等公民”(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

  1. 只用”表达式”,不用”语句”

“表达式”(expression)是一个单纯的运算过程,总是有返回值;”语句”(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。

原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。”语句”属于对系统的读写操作,所以就被排斥在外。

当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

  1. 无副作用

函数式编程强调没有”副作用”,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

  1. 不修改状态

指的是一个变量一旦创建后,就不能再进行修改,任何修改都会生成一个新的变量。使用不可变变量最大的好处是线程安全。多个线程可以同时访问同一个不可变变量,让并行变得更容易实现。

  1. 引用透明

引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或”状态”,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

函数与Lambda 表达式

函数

函数作为Kotlin中的一等公民,可以像其他对象一样作为函数的输入与输出。关于对函数式编程的支持,相对于Scala的学院派风格,Kotlin则是纯的的工程派:实用性、简洁性上都要比Scala要好。

首先看一下Kotlin中的一些典型函数

基本函数
// 有返回值
fun getTriple(x: Int): Int {
    return 3 * x
}


// 无返回值
fun printHello(name: String?): Unit {
    if (name != null)
    println("Hello ${name}")
    else
    println("Hi there!")
    // `return Unit` 或者 `return` 是可选的
}

单表达式函数
fun getTriple(x: Int): Int = x * 3

// 返回值类型可由编译器推断时
fun getTriple(x: Int) = x * 3
顶层函数

见名知意,原来在Java中,类处于顶层,类包含属性和方法,在Kotlin中,函数站在了类的位置,我们可以直接把函数放在代码文件的顶层,让它不从属于任何类

// Sample.kt 文件中

fun getTriple(x: Int) = x * 3    // 此函数不属于任何类

class Sample{
                ……
}
中缀函数
infix fun Int.shl(x: Int): Int {
…… 

}

// 用中缀表示法调用该函数
1 shl 2

// 等同于这样
1.shl(2)

标有 infix 关键字的函数也可以使用中缀表示法(忽略该调用的点与圆括号)调用。中缀函数必须满足以下要求:

  1. 它们必须是成员函数或扩展函数;
  2. 它们必须只有一个参数;
  3. 其参数不得接受可变数量的参数且不能有默认值。

!注意,中缀函数总是要求指定接收者与参数。当使用中缀表示法在当前接收者上调用方法时,需要显式使用 this;不能像常规方法调用那样省略。这是确保非模糊解析所必需的。

局部函数

Kotlin 支持局部函数,即一个函数在另一个函数内部,局部函数可以访问外部函数(即闭包)的局部变量

fun dfs(graph: Graph) {
    val visited = HashSet<Vertex>()
    fun dfs(current: Vertex) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v)
    }

    dfs(graph.vertices[0])
}
泛型函数

函数可以有泛型参数,通过在函数名前使用尖括号指定:

fun <T> getList(item: T): List<T> { …… }
尾递归函数

Kotlin 支持一种称为尾递归的函数式编程风格。 这允许一些通常用循环写的算法改用递归函数来写,而无堆栈溢出的风险。 当一个函数用 tailrec 修饰符标记并满足所需的形式时,编译器会优化该递归,留下一个快速而高效的基于循环的版本:

tailrec fun findFixPoint(x: Double = 1.0): Double
        = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x))

这段代码计算余弦的不动点(fixpoint of cosine),这是一个数学常数。 它只是重复地从 1.0 开始调用 Math.cos,直到结果不再改变,产生0.7390851332151607的结果。最终代码相当于这种更传统风格的代码:

private fun findFixPoint(): Double {
    var x = 1.0
    while (true) {
        val y = Math.cos(x)
        if (x == y) return x
        x = y
    }
}

要符合 tailrec 修饰符的条件的话,函数必须将其自身调用作为它执行的最后一个操作。在递归调用后有更多代码时,不能使用尾递归,并且不能用在 try/catch/finally 块中。目前尾部递归只在 JVM 后端中支持。

扩展函数

Kotlin 同 C# 和 Gosu 类似,能够扩展一个类的新功能而无需继承该类或使用像装饰者这样的任何类型的设计模式。 这通过叫做 扩展 的特殊声明完成。

声明一个扩展函数,我们需要用一个接收者类型 也就是被扩展的类型来作为他的前缀。 下面代码为 ArrayList<Int> 添加一个swap 函数:

fun ArrayList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // “this”对应该ArrayList
    this[index1] = this[index2]
    this[index2] = tmp
}
内联函数

调用一个方法是一个压栈和出栈的过程,调用方法时将栈针压入方法栈,然后执行方法体,方法结束时将栈针出栈,这个压栈和出栈的过程会耗费资源,这个过程中传递形参也会耗费资源。 针对这个问题,kotlin引入了inline关键字即内联。

但是也不是说所有的函数都要内联,因为一旦添加了inline修饰,在编译阶段,编译器将会把函数拆分,插入到调用出。如果一个 inline 函数是很大的,那他会大幅增加调用它的那个函数的体积。

inline fun <T> lock(lock: Lock, body: () -> T): T { …… }
高阶函数

高阶函数是将函数用作参数或返回值的函数。

前面说函数在Kotlin中是一等公民,它要像其他类型一样使用,所以为了实现这个Kotlin使用函数类型来表示函数并提供了一种特定的结构。

函数类型定义:
  参数 -> 返回值类型
  • 所有函数类型都有一个圆括号括起来的参数类型列表以 及一个返回类型:(A, B) -> C表示接受类型分别为 A 与 B 两个参数并返回一个 C 类型值的函数类型。 参数类型列表可以为空,如 () -> A。Unit 返回类型不可省略。

  • 函数类型可以有一个额外的接收者类型,它在表示法中的点之前指定: 类型 A.(B) -> C 表示可以在 A 的接收者对象上以一个 B 类型参数来调用并返回一个 C 类型值的函数。 带有接收者的函数字面值通常与这些类型一起使用。

  • 挂起函数属于特殊种类的函数类型,它的表示法中有一个 suspend 修饰符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C

(因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。挂起函数能够以与普通函数相同的方式获取参数与返回值,但它们只能从协程、其他挂起函数以及内联到其中的函数字面值中调用。)

函数类型表示法可以选择性地包含函数的参数名:(x: Int, y: Int) -> Point。 这些名称可用于表明参数的含义。

  • 如需将函数类型指定为可空,请使用圆括号:((Int, Int) -> Int)?。
  • 函数类型可以使用圆括号进行接合:(Int) -> ((Int) -> Unit)
  • 箭头表示法是右结合的,(Int) -> (Int) -> Unit 与前述示例等价,但不等于 ((Int) -> (Int)) -> Unit。
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

// contractContracts DSL 它可以为编译器提供关于函数行为的附加信息,帮助编译器分析函数的实际运行情况。

let 可以被任何对象调用。它接收一个函数作为参数,作为参数的函数返回的结果作为整个函数的返回值。

public inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R): R {
    var accumulator = initial
    for (element in this) accumulator = operation(accumulator, element)
    return accumulator
}

参数 combine 具有函数类型 (R, T) -> R,因此 fold 接受一个函数作为参数, 该函数接受类型分别为 R 与 T 的两个参数并返回一个 R 类型的值。 在 for-循环内部调用该函数,然后将其返回值赋值给 accumulator。

函数类型实例化:
  • 使用函数字面值的代码块,采用以下形式之一:
    • lambda 表达式: { a, b -> a + b }
    • 匿名函数: fun(s: String): Int { return s.toIntOrNull() ?: 0 }

带有接收者的函数字面值可用作带有接收者的函数类型的值。

  • 使用已有声明的可调用引用:
    • 顶层、局部、成员、扩展函数:::isOdd、 String::toInt,
    • 顶层、成员、扩展属性:List::size,
    • 构造函数:::Regex

这包括指向特定实例成员的绑定的可调用引用:foo::toString。

  • 使用实现函数类型接口的自定义类的实例

    class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
    }
    
    val intFunction: (Int) -> Int = IntTransformer()
函数类型实例调用

函数类型的值可以通过其 invoke(……) 操作符调用:f.invoke(x) 或者直接 f(x)

如果该值具有接收者类型,那么应该将接收者对象作为第一个参数传递。

匿名函数

上面提供的 lambda表达式语法缺少的一个东西是指定函数的返回类型的能力。在大多数情况下,这是不必要的。因为返回类型可以自动推断出来。然而,如果确实需要显式指定,可以使用另一种语法: 匿名函数 。

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

匿名函数看起来非常像一个常规函数声明,除了其名称省略了。其函数体可以是表达式(如上所示)或代码块:

fun(x: Int, y: Int): Int {
    return x + y
}

参数和返回类型的指定方式与常规函数相同,除了能够从上下文推断出的参数类型可以省略:

匿名函数的返回类型推断机制与正常函数一样:对于具有表达式函数体的匿名函数将自动推断返回类型,而具有代码块函数体的返回类型必须显式指定(或者已假定为 Unit)。

请注意,匿名函数参数总是在括号内传递。 允许将函数留在圆括号外的简写语法仅适用于 lambda 表达式。

Lambda表达式与匿名函数之间的另一个区别是非局部返回的行为。一个不带标签的 return 语句总是在用 fun 关键字声明的函数中返回。这意味着 lambda 表达式中的 return 将从包含它的函数返回,而匿名函数中的 return 将从匿名函数自身返回。

Lambda 表达式

Lambda表达式基于数学中的λ演算得名

Lambda 表达式是一个匿名函数, 但立即做为表达式传递。

Lambda 表达式语法:

{ 参数 -> 函数体}

Lambda 表达式的完整语法形式举例如下:

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

lambda 表达式总是括在花括号中, 完整语法形式的参数声明放在花括号内,并有可选的类型标注, 函数体跟在一个 -> 符号之后。如果推断出的该 lambda 的返回类型不是 Unit,那么该 lambda 主体中的最后一个(或可能是单个)表达式会视为返回值。

Lambda的一些语法糖
  1. 如果函数的最后一个参数接受函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外:

  2. 当参数只有一个的时候,声明中可以不用显示声明参数,在使用参数时可以用 it 来替代那个唯一的参数。

  3. 当有多个用不到的参数时,可以用下划线来替代参数名(1.1以后的特性),但是如果已经用下划线来省略参数时,是不能使用 it 来替代当前参数的。
  4. 可以使用限定的返回语法从lambda显式返回一个值。 否则,将隐式返回最后一个表达式的值。

Kotlin 的 Lambda表达式更“纯粹”一点, 因为它是真正把Lambda抽象为了一种类型,而 Java 8 的 Lambda 只是单方法匿名接口实现的语法糖罢了。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值