Kotlin-函数式指南-全-

Kotlin 函数式指南(全)

原文:zh.annas-archive.org/md5/554454848b9d9cb3734b502ec33fb8ec

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

随着 Google 在 2017 年 I/O 大会上的宣布,将 Kotlin 定为 Android 的官方语言,Kotlin 在全球开发者中的受欢迎程度正在上升。

然而,Kotlin 的使用和流行并不局限于 Android 社区。许多其他社区,如桌面、Web 和后端社区,也在接受 Kotlin。许多新的库和框架正在被创建,现有的也在为 Kotlin 提供支持。

随着越来越多的开发者加入 Kotlin 社区,以及其自然的灵活性,更多的编程风格正在被尝试。本书的目的是向广泛的 Kotlin 社区介绍函数式编程风格,引领和指导初学者,并提供基本工具以进一步学习更高级的概念。

本书面向的对象

这本书是为 Kotlin 用户(程序员、工程师、库作者和架构师)而写的,他们已经对 Kotlin 有基本的了解,并希望了解函数式编程的基本理念以及如何在实践中使用它(如果你对 Kotlin 完全陌生,我们的附录《Kotlin 快速入门》将为你提供语言快速入门)。

本书涵盖的内容

第一章,Kotlin - 数据类型、对象和类,介绍了 Kotlin 中的面向对象编程。Kotlin 主要是一种面向对象编程语言,我们将使用这些特性来引入函数式编程风格。

第二章,开始使用函数式编程,涵盖了使用 Kotlin 的面向对象编程特性来介绍函数式编程的基本原则。

第三章,不可变性 - 它很重要,强调不可变性是函数式编程中最重要概念之一。本章将深入讲解不可变性。

第四章,函数、函数类型和副作用,介绍了围绕函数、纯函数以及各种函数类型和副作用的函数式编程基本概念。

第五章,关于函数的更多内容,讨论了 Kotlin 在函数式编程方面的特性,例如扩展函数、操作符重载、DSL 和核心递归。

第六章,Kotlin 中的委托,介绍了 Kotlin 如何在语言级别支持委托。尽管委托是一种面向对象编程概念,但它们可以帮助使你的代码更加模块化。

第七章,使用协程进行异步编程,介绍了 Kotlin 中的异步编程,并比较了协程与其他不同风格的对比。

第八章,Kotlin 中的集合和数据操作,介绍了 Kotlin 的增强集合 API 和 Kotlin 集合框架提供的功能接口。

第九章,函数式编程和响应式编程,展示了如何将函数式编程与其他编程范式结合以获得最佳效果。本章讨论了您如何将函数式编程与面向对象编程和响应式编程结合使用。

第十章,函子、应用和单子,为您介绍了类型化函数式编程及其基本概念。它还讨论了如何在 Kotlin 中实现它。

第十一章,在 Kotlin 中使用流,为您介绍了 Kotlin 中的 Streams API。

第十二章,使用 Arrow 入门,介绍了如何使用 Arrow 及其扩展进行函数式编程、函数组合、柯里化、部分应用、记忆化和光学。

第十三章,箭头类型,帮助您理解箭头数据类型,如 Option、Either、Try 和 State 及其类型类、函子和单子。

附录,Kotlin 快速入门,提供了您开始编写 Kotlin 代码所需的一切,例如工具、基本语法结构以及其他资源,以帮助您在 Kotlin 之旅中进步。

要充分利用这本书

运行和编写 Kotlin 程序唯一推荐的软件是 IntelliJ IDEA(还有其他方法可以做到这一点,我们将在附录 Kotlin 快速入门 中介绍)。

您可以从 www.jetbrains.com/idea/download/ 下载 IntelliJ IDEA。

您可以在 Windows、Mac 和 Linux 上安装 IntelliJ IDEA:

  • 对于 Windows:您可以使用从 XP 到 10 的任何 Windows 版本。要在 Windows 上安装它,运行安装程序可执行文件并遵循说明。

  • 对于 Mac:您可以使用从 10.8 开始的任何 macOS 版本。要在 macOS 上安装它,挂载磁盘映像文件并将 IntelliJ IDEA.app**60 复制到您的 Application 文件夹。

  • 对于 Linux:您可以使用任何 GNOME 或 KDE 桌面。要在 Linux 上安装它,使用 tar -xzf idea-.tar.gz* 命令解压缩 tar.gz 文件,然后从 bin 子目录中运行 idea.sh

下载示例代码文件

您可以从 www.packtpub.com 的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com 上登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

文件下载后,请确保使用最新版本解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Functional-Kotlin。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包可供在 github.com/PacktPublishing/ 上获取。去看看吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载: www.packtpub.com/sites/default/files/downloads/FunctionalKotlin_ColorImages.pdf

使用约定

本书使用了多种文本约定。

CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“我们引入了一个新的BakeryGood类,它具有CupcakeBiscuit类共享的行为和状态,并且我们让这两个类都扩展了BakeryGood。”

代码块设置如下:

open class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour bakery good" 
  } 
} 

class Cupcake(flavour: String): BakeryGood(flavour) 
class Biscuit(flavour: String): BakeryGood(flavour)

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

fun main(args: Array<String>) { 
 val emptyList1 = listOf<Any>() val emptyList2 = emptyList<Any>() 

    println("emptyList1.size = ${emptyList1.size}") 
    println("emptyList2.size = ${emptyList2.size}") 
} 

任何命令行输入或输出都按以下方式编写:

$ kotlin HelloKt

粗体: 表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“将出现一个对话框,询问您是否想将其打开为文件或项目。点击 Open As Project”

警告或重要注意事项如下所示。

技巧和技巧如下所示。

联系我们

我们始终欢迎读者的反馈。

一般反馈: 请通过电子邮件 feedback@packtpub.com 发送反馈,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过电子邮件 questions@packtpub.com 联系我们。

勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件 copyright@packtpub.com 与我们联系,并附上材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com.

评论

请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

关于 Packt 的更多信息,请访问 packtpub.com

第一章:Kotlin – 数据类型、对象和类

在本章中,我们将介绍 Kotlin 的类型系统、面向对象编程(OOP)与 Kotlin、修饰符、解构声明等。

Kotlin 主要是一种面向对象(OOP)语言,同时具有一些函数式特性。当我们使用面向对象的语言来解决问题时,我们试图以与问题相关的信息以抽象的方式模拟问题中的一部分对象。

如果我们为公司设计一个 HR 模块,我们将用状态或数据(姓名、出生日期、社会保险号等)和行为(支付薪水、调到另一个部门等)来模拟员工。因为一个人可能非常复杂,有些信息对于我们的问题或领域并不相关。例如,员工的自行车喜好风格对于我们的 HR 系统并不相关,但对于在线自行车店来说却非常相关。

一旦我们确定了对象(包括数据和行为)以及它们与我们领域其他对象的关系,我们就可以开始开发和编写代码,这些代码将成为我们软件解决方案的一部分。我们将使用语言结构(结构是一种说法,指的是允许的语法)来编写对象、类别、关系等等。

Kotlin 有许多我们可以用来编写程序的构造,在本章中,我们将介绍其中许多构造,例如:

  • 继承

  • 抽象类

  • 接口

  • 对象

  • 泛型

  • 类型别名

  • 空类型

  • Kotlin 的类型系统

  • 其他类型

是 Kotlin 中的基础类型。在 Kotlin 中,类是一个模板,它为实例提供状态、行为和类型(稍后会有更多介绍)。

定义一个类只需要一个名称:

class VeryBasic

VeryBasic不是非常有用,但仍然是一个有效的 Kotlin 语法。

VeryBasic类没有任何状态或行为;尽管如此,你仍然可以声明VeryBasic类型的值,如下面的代码所示:

fun main(args: Array<String>) {
    val basic: VeryBasic = VeryBasic()
}

正如你所见,basic值具有VeryBasic类型。用不同的方式表达,basicVeryBasic的一个实例。

在 Kotlin 中,类型可以被推断;因此,前面的例子等同于以下代码:

fun main(args: Array<String>) {
    val basic = VeryBasic()
}

由于basic是一个VeryBasic实例,它具有VeryBasic类型的副本状态和行为,即没有。真是太遗憾了。

属性

如前所述,类可以有状态。在 Kotlin 中,类状态由属性表示。让我们看看蓝莓纸杯蛋糕的例子:

class BlueberryCupcake {
  var flavour = "Blueberry"
}

BlueberryCupcake类有一个has-a属性flavour,其类型为String

当然,我们可以有BlueberryCupcake类的实例:

fun main(args: Array<String>) {
    val myCupcake = BlueberryCupcake()
    println("My cupcake has ${myCupcake.flavour}")
}

现在,因为我们声明了flavour属性为变量,它的内部值可以在运行时被改变:

fun main(args: Array<String>) {
    val myCupcake = BlueberryCupcake()
    myCupcake.flavour = "Almond"
    println("My cupcake has ${myCupcake.flavour}")
}

在现实生活中这是不可能的。纸杯蛋糕不会改变它们的味道(除非它们变陈了)。如果我们将flavour属性更改为一个值,它就不能被修改:

class BlueberryCupcake {
    val flavour = "Blueberry"
}

fun main(args: Array<String>) {
    val myCupcake = BlueberryCupcake()
    myCupcake.flavour = "Almond" //Compilation error: Val cannot be reassigned
    println("My cupcake has ${myCupcake.flavour}")
}

让我们声明一个新的类来表示杏仁纸杯蛋糕:

class AlmondCupcake {
    val flavour = "Almond"
}

fun main(args: Array<String>) {
    val mySecondCupcake = AlmondCupcake()
    println("My second cupcake has ${mySecondCupcake.flavour} flavour")
}

这里有些奇怪。BlueberryCupcakeAlmondCupcake在结构上相同;只是内部值发生了变化。

在现实生活中,你不需要为不同的纸杯蛋糕风味准备不同的烤盘。同一个高质量的烤盘可以用于各种风味。同样,一个设计良好的Cupcake类可以用于不同的实例:

class Cupcake(flavour: String) { 
  val flavour = flavour
}

Cupcake类有一个带有参数flavour的构造函数,该参数被分配给flavour值。

因为这是一个非常常见的习语,Kotlin 有一些语法糖来更简洁地定义它:

class Cupcake(val flavour: String)

现在,我们可以定义具有不同风味的几个实例:

fun main(args: Array<String>) {
    val myBlueberryCupcake = Cupcake("Blueberry")
    val myAlmondCupcake = Cupcake("Almond")
    val myCheeseCupcake = Cupcake("Cheese")
    val myCaramelCupcake = Cupcake("Caramel")
}

方法

在 Kotlin 中,一个类的行为由方法定义。技术上,方法是一个成员函数,因此,我们在以下章节中学到的关于函数的知识也适用于方法:

class Cupcake(val flavour: String) {
  fun eat(): String {
    return "nom, nom, nom... delicious $flavour cupcake"
  }
}

eat()方法返回一个String值。现在,让我们调用eat()方法,如下面的代码所示:

fun main(args: Array<String>) {
    val myBlueberryCupcake = Cupcake("Blueberry")
    println(myBlueberryCupcake.eat())
}

以下表达式是前面代码的输出:

没有什么令人震惊的,但这是我们第一个方法。稍后,我们会做更多有趣的事情。

继承

随着我们继续在 Kotlin 中建模我们的领域,我们发现具体对象相当相似。如果我们回到我们的人力资源示例,员工和承包商相当相似;他们都有名字、出生日期等;他们也有一些差异。例如,承包商有日工资,而员工有月薪。很明显,他们是相似的——他们都是人;人是包含承包商和员工的超集。因此,他们都有自己独特的特征,使他们足够不同,可以分类到不同的子集中。

这就是继承的全部内容,有组和子组,它们之间有联系。在继承层次结构中,如果你向上移动层次结构,你会看到更多通用特性和行为,如果你向下移动,你会看到更具体的行为。玉米卷和微处理器都是对象,但它们有不同的用途和用途。

让我们引入一个新的Biscuit类:

class Biscuit(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour biscuit" 
  } 
}

再次,这个类看起来几乎与Cupcake完全相同。我们可以重构这些类以减少代码重复:

open class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour bakery good" 
  } 
} 

class Cupcake(flavour: String): BakeryGood(flavour) 
class Biscuit(flavour: String): BakeryGood(flavour)

我们引入了一个新的BakeryGood类,它具有CupcakeBiscuit类的共享行为和状态,并且我们让这两个类都扩展了BakeryGood。通过这样做,Cupcake(和Biscuit)现在与BakeryGood有了一个关系;另一方面,BakeryGoodCupcake类的超类或父类。

注意BakeryGood被标记为open。这意味着我们特别设计了这个类以便扩展。在 Kotlin 中,你不能扩展一个不是open的类。

将常见的行为和状态移动到父类的过程称为泛化。让我们看一下下面的代码:

fun main(args: Array<String>) {
    val myBlueberryCupcake: BakeryGood = Cupcake("Blueberry")
    println(myBlueberryCupcake.eat())
}

让我们尝试我们的新代码:

图片

唉,这不是我们预期的。我们需要进一步折射它:

open class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  open fun name(): String { 
    return "bakery good" 
  } 
} 

class Cupcake(flavour: String): BakeryGood(flavour) { 
  override fun name(): String { 
    return "cupcake" 
  } 
} 

class Biscuit(flavour: String): BakeryGood(flavour) { 
  override fun name(): String { 
    return "biscuit" 
  } 
}

它工作了!让我们看看输出结果:

图片

我们声明了一个新的方法,name();它应该被标记为 open,因为我们设计它可以在其子类中可选地修改。

在子类中修改方法定义称为 重写,这就是为什么两个子类中的 name() 方法都被标记为 override

在层次结构中扩展类和重写行为的过程称为 特殊化

经验法则

将一般状态和行为放在层次结构的顶部(泛化),将特定状态和行为放在子类中(特殊化)。

现在,我们可以有更多的面包店商品了!让我们看看下面的代码:

open class Roll(flavour: String): BakeryGood(flavour) { 
  override fun name(): String { 
    return "roll" 
  } 
} 

class CinnamonRoll: Roll("Cinnamon")

子类也可以被扩展。它们只需要被标记为 open

open class Donut(flavour: String, val topping: String) : BakeryGood(flavour)
{
    override fun name(): String {
        return "donut with $topping topping"
    }
}

fun main(args: Array<String>) {
    val myDonut = Donut("Custard", "Powdered sugar")
    println(myDonut.eat())
}

我们还可以创建具有更多属性和方法的方法。

抽象类

到目前为止,一切顺利。我们的面包店看起来不错。然而,我们当前模型有一个问题。让我们看看下面的代码:

fun main(args: Array<String>) {
    val anyGood = BakeryGood("Generic flavour")
}

我们可以直接实例化 BakeryGood 类,这太通用。为了纠正这种情况,我们可以将 BakeryGood 标记为 abstract

abstract class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  }

  open fun name(): String { 
    return "bakery good" 
  } 
}

抽象类 是一个专为扩展而设计的类。抽象类不能被实例化,这解决了我们的问题。

什么是 abstractopen 的不同之处?

这两个修饰符都允许我们扩展一个类,但 open 允许我们实例化,而 abstract 不允许。

现在我们不能实例化,BakeryGood 类中的 name() 方法就不再那么有用,而且除了 CinnamonRoll 之外的所有子类都重写了它(CinnamonRoll 依赖于 Roll 的实现):

abstract class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  abstract fun name(): String 
}

被标记为 abstract 的方法没有主体,只有签名声明(方法签名是识别方法的一种方式)。在 Kotlin 中,签名由方法名、参数数量、参数类型和返回类型组成。

任何直接扩展 BakeryGood 的类都必须重写 name() 方法。重写抽象方法的术语是 实现,从现在起,我们将使用它。所以,Cupcake 类实现了 name() 方法(Kotlin 没有用于方法实现的关键字;方法实现和方法重写都使用关键字 override)。

让我们引入一个新的类,Customer;面包店总是需要顾客的:

class Customer(val name: String) {
  fun eats(food: BakeryGood) {
    println("$name is eating... ${food.eat()}")
  }
}

fun main(args: Array<String>) {
    val myDonut = Donut("Custard", "Powdered sugar")
    val mario = Customer("Mario")
    mario.eats(myDonut)
}

eats(food: BakeryGood) 方法接受一个 BakeryGood 参数,因此任何扩展 BakeryGood 参数的类的实例,无论有多少层等级。只需记住,我们可以直接实例化 BakeryGood

如果我们想要一个简单的 BakeryGood 呢?例如,进行测试。

有一个替代方案,一个匿名子类:

fun main(args: Array<String>) {
    val mario = Customer("Mario")

    mario.eats(object : BakeryGood("TEST_1") {
        override fun name(): String {
            return "TEST_2"
        }
    })
}

这里引入了一个新关键字,object。稍后我们将更详细地介绍 object,但就目前而言,只需知道这是一个 对象表达式。对象表达式定义了一个匿名类的实例,该类扩展了一个类型。

在我们的例子中,对象表达式(技术上,是 匿名类)必须重写 name() 方法,并将值作为参数传递给 BakeryGood 构造函数,就像标准类一样。

记住,一个 object 表达式是一个实例,因此它可以用来声明值:

val food: BakeryGood = object : BakeryGood("TEST_1") { 
  override fun name(): String { 
    return "TEST_2" 
  } 
} 

mario.eats(food)

接口

公开和抽象类非常适合创建层次结构,但有时它们不够用。一些子集可能跨越看似无关的层次结构,例如,鸟类和大型灵长类动物都是两足动物,它们都是动物和脊椎动物,但它们并不直接相关。这就是为什么我们需要不同的结构,Kotlin 给我们提供了接口(其他语言以不同的方式处理这个问题)。

我们的面包店产品很棒,但我们需要先烹饪它们:

abstract class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  fun bake(): String { 
    return "is hot here, isn't??" 
  } 

  abstract fun name(): String 
}

我们的新的 bake() 方法将烹饪我们所有惊人的产品,但是等等,甜甜圈不是烘焙的,而是油炸的。

如果我们能把 bake() 方法移动到第二个抽象类 Bakeable 中会怎样?让我们在下面的代码中尝试一下:

abstract class Bakeable { 
  fun bake(): String { 
    return "is hot here, isn't??" 
  } 
} 

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable() { //Compilation error: Only one class may appear in a supertype list 
  override fun name(): String { 
    return "cupcake" 
  } 
}

错误!在 Kotlin 中,一个类不能同时扩展两个类。让我们看看下面的代码:

interface Bakeable { 
  fun bake(): String { 
    return "is hot here, isn't??" 
  } 
} 

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable { 
  override fun name(): String { 
    return "cupcake" 
  } 
}

然而,它可以扩展多个接口。一个 接口 是一个定义行为的类型;在 Bakeable 接口的情况下,那就是 bake() 方法。

因此,公开/抽象类和接口之间的区别是什么?

让我们从以下相似之处开始:

  • 它们都是类型。在我们的例子中,CupcakeBakeryGood 有一个 is-a 关系,并且与 Bakeable 也有一个 is-a 关系。

  • 它们都通过方法定义行为。

  • 虽然公开类可以直接实例化,但抽象类和接口不行。

现在,让我们看看以下的不同之处:

  • 一个类可以扩展一个类(公开或抽象),但不能扩展多个接口。

  • 公开/抽象类可以有构造函数。

  • 公开/抽象类可以初始化自己的值。接口的值必须在扩展接口的类中初始化。

  • 公开类必须声明可以重写的公开方法。抽象类可以同时有公开和抽象方法。

在接口中,所有方法都是公开的,没有实现的方法不需要抽象修饰符:

interface Fried { 
  fun fry(): String 
} 

open class Donut(flavour: String, val topping: String) : BakeryGood(flavour), Fried { 
  override fun fry(): String { 
    return "*swimming on oil*" 
  } 

  override fun name(): String { 
    return "donut with $topping topping" 
  } 
}

何时使用其中一个或另一个?:

  • 当使用公开/抽象类时:

    • 这个类应该被扩展和实例化
  • 当需要应用多重继承时使用抽象类:

    • 这个类不能被实例化

    • 需要一个构造函数

    • 存在初始化逻辑(使用 init 块)

让我们看看以下代码:

abstract class BakeryGood(val flavour: String) {
  init { 
    println("Preparing a new bakery good") 
  } 

  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  abstract fun name(): String 
}
  • 当需要使用接口时:

    • 使用公开类:

    • 不需要初始化逻辑

我的建议是,你应该始终从接口开始。接口更直接、更简洁;它们还允许更模块化的设计。如果需要数据初始化/构造函数,则可以转向抽象/开放的。

与抽象类一样,对象表达式可以与接口一起使用:

val somethingFried = object : Fried { 
  override fun fry(): String { 
    return "TEST_3" 
  } 
}

对象

我们已经介绍了对象表达式,但关于对象还有更多内容。对象是自然的单例(这里的“自然”是指作为语言特性出现,而不是作为行为模式实现,如其他语言中的情况)。单例是一种只有一个实例的类型,并且 Kotlin 中的每个对象都是单例。这打开了许多有趣的模式(以及一些不良实践)。作为单例的对象对于协调系统中的操作很有用,但如果不小心使用来保持全局状态,也可能很危险。

对象表达式不需要扩展任何类型:

fun main(args: Array<String>) {
    val expression = object {
        val property = ""

        fun method(): Int {
            println("from an object expressions")
            return 42
        }
    }

    val i = "${expression.method()} ${expression.property}"
    println(i)
}

在这种情况下,expression 值是一个没有特定类型的对象。我们可以访问其属性和函数。

有一个限制——没有类型的对象表达式只能在本地使用,即在方法内部,或者私有地,在类内部:

class Outer {
    val internal = object {
        val property = ""
    }
}

fun main(args: Array<String>) {
    val outer = Outer()

    println(outer.internal.property) // Compilation error: Unresolved reference: property
}

在这种情况下,property 值无法访问。

对象声明

对象也可以有一个名称。这种对象被称为 对象声明

object Oven {
  fun process(product: Bakeable) {
    println(product.bake())
  }
}

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake("Almond")
    Oven.process(myAlmondCupcake)
}

对象是单例;你不需要实例化 Oven 来使用它。对象还可以扩展其他类型:

interface Oven {
  fun process(product: Bakeable)
}

object ElectricOven: Oven {
  override fun process(product: Bakeable) {
    println(product.bake())
  }
}

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake("Almond")
    ElectricOven.process(myAlmondCupcake)
}

伴随对象

在类/接口内部声明的对象可以被标记为伴随对象。观察以下代码中伴随对象的使用:

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable {
  override fun name(): String { 
    return "cupcake" 
  } 

  companion object { 
    fun almond(): Cupcake { 
      return Cupcake("almond") 
    } 

    fun cheese(): Cupcake { 
      return Cupcake("cheese") 
    } 
  } 
}

现在,可以直接使用类名来使用伴随对象中的方法,而不需要实例化它:

fun main(args: Array<String>) {
    val myBlueberryCupcake: BakeryGood = Cupcake("Blueberry")
    val myAlmondCupcake = Cupcake.almond()
    val myCheeseCupcake = Cupcake.cheese()
    val myCaramelCupcake = Cupcake("Caramel")
}

伴随对象的方法不能从实例中使用:

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake.almond()
    val myCheeseCupcake = myAlmondCupcake.cheese() //Compilation error: Unresolved reference: cheese
}

伴随对象可以作为具有名称 Companion 的值在类外部使用:

fun main(args: Array<String>) {
    val factory: Cupcake.Companion = Cupcake.Companion
}

或者,Companion 对象也可以有一个名称:

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable {
    override fun name(): String {
        return "cupcake"
    }

    companion object Factory {
        fun almond(): Cupcake {
            return Cupcake("almond")
        }

        fun cheese(): Cupcake {
            return Cupcake("cheese")
        }
    }
}

fun main(args: Array<String>) {
    val factory: Cupcake.Factory = Cupcake.Factory
}

它们也可以不命名使用,如下面的代码所示:

fun main(args: Array<String>) {
    val factory: Cupcake.Factory = Cupcake
}

不要被这种语法弄混淆。没有括号的 Cupcake 值是伴随对象;Cupcake() 是一个实例。

泛型

这一部分只是泛型的一个简要介绍;稍后我们将详细讨论。

泛型编程是一种编程风格,它侧重于创建适用于一般问题的算法(以及相关数据结构)。

Kotlin 支持泛型编程的方式是使用类型参数。简而言之,我们用类型参数编写代码,然后在需要使用时,将这些类型作为参数传递。

以我们的 Oven 接口为例:

interface Oven {
  fun process(product: Bakeable)
}

烤箱是一种机器,因此我们可以更广泛地推广它:

interface Machine<T> {
  fun process(product: T)
}

Machine<T> 接口定义了一个类型参数 T 和一个 process(T) 方法。

现在,我们可以用 Oven 来扩展它:

interface Oven: Machine<Bakeable>

现在,Oven 使用 Bakeable 类型参数扩展了 Machine,因此 process 方法现在接受 Bakeable 作为参数。

类型别名

类型别名提供了一种定义已存在类型名称的方法。类型别名可以帮助使复杂类型更容易阅读,并且还可以提供其他提示。

在某种意义上,Oven 接口只是一个名称,代表一个 Machine<Bakeable>

typealias Oven = Machine<Bakeable>

我们新的类型别名 Oven 与我们熟悉的 Oven 接口完全一样。它可以被扩展,并具有 Oven 类型的值。

类型别名也可以用来增强类型信息,提供与你的领域相关的有意义的名称:

typealias Flavour = String

abstract class BakeryGood(val flavour: Flavour) {

它也可以用于集合:

typealias OvenTray = List<Bakeable>

它也可以与对象一起使用:

typealias CupcakeFactory = Cupcake.Companion

可空类型

Kotlin 的一个主要特性是可空类型。可空类型允许我们显式地定义一个值是否可以包含或为空:

fun main(args: Array<String>) {
    val myBlueberryCupcake: Cupcake = null //Compilation error: Null can not be a value of a non-null type Cupcake
}

在 Kotlin 中这并不有效;Cupcake 类型不允许空值。要允许空值,myBlueberryCupcake 必须有不同的类型:

fun main(args: Array<String>) {
    val myBlueberryCupcake: Cupcake? = null
}

从本质上讲,Cupcake 是一个非空类型,而 Cupcake? 是一个可空类型。

在层次结构中,CupcakeCupcake? 的子类型。因此,在任何 Cupcake? 被定义的情况下,Cupcake 可以被使用,但反之则不行:

fun eat(cupcake: Cupcake?){
//  something happens here    
}

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake.almond()

    eat(myAlmondCupcake)

    eat(null)
}

Kotlin 编译器在可空类型和非空类型实例之间做出区分。

让我们以这些值为例:

fun main(args: Array<String>) {
    val cupcake: Cupcake = Cupcake.almond()
    val nullabeCupcake: Cupcake? = Cupcake.almond()
}

接下来,我们将对可空类型和非空类型都调用 eat() 方法:

fun main(args: Array<String>) {
    val cupcake: Cupcake = Cupcake.almond()
    val nullableCupcake: Cupcake? = Cupcake.almond()

    cupcake.eat() // Happy days
    nullableCupcake.eat() //Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Cupcake?
}

cupcake 上调用 eat() 方法就像吃馅饼一样简单;在 nullableCupcake 上调用 eat() 会产生编译错误。

为什么?对于 Kotlin 来说,从一个可空值中调用方法是有风险的,可能会抛出一个潜在的 NullPointerException(以下简称 NPE)。因此,为了安全起见,Kotlin 将其标记为编译错误。

如果我们真的想从一个可空值中调用一个方法或访问一个属性,会发生什么?

好吧,Kotlin 提供了处理可空值的选择,所有这些都是显式的。在某种程度上,Kotlin 在告诉你,“展示给我,你知道你在做什么”。

让我们回顾一些选项(在接下来的章节中我们还将介绍更多选项)。

检查空值

if 块中将空值检查作为一个条件:

fun main(args: Array<String>) {
    val nullableCupcake: Cupcake? = Cupcake.almond()

    if (nullableCupcake != null) {
      nullableCupcake.eat()
    }
}

Kotlin 将执行智能转换。在 if 块内部,nullableCupcakeCupcake,而不是 Cupcake?;因此,可以访问任何方法或属性。

检查非空类型

这与上一个类似,但它直接检查类型:

if (nullableCupcake is Cupcake) {
  nullableCupcake.eat()
}

它也可以与 when 一起使用:

when (nullableCupcake) {
  is Cupcake -> nullableCupcake.eat()
}

两种选项,检查空值和非空类型,都有些冗长。让我们看看其他选项。

安全调用

安全调用允许在值非空时访问可空值的属性和方法(在底层,在字节码级别,安全调用被转换为 if(x != null)):

nullableCupcake?.eat()

但是,如果你在表达式中使用它呢?

val result: String? = nullableCupcake?.eat()

如果我们的值是空,它将返回空,所以 result 必须有 String? 类型。

这样就打开了在链上使用安全调用的机会,如下所示:

val length: Int? = nullableCupcake?.eat()?.length

Elvis (?😃 操作符

Elvis 操作符(?:)在表达式中使用空值时返回一个替代值:

val result2: String = nullableCupcake?.eat() ?: ""

如果nullabluCupcake?.eat()null,则?:运算符将返回替代值""

显然,Elvis 运算符可以与一系列安全调用一起使用:

val length2: Int = nullableCupcake?.eat()?.length ?: 0

!!!运算符

而不是null值,!!运算符将抛出一个 NPE:

val result: String = nullableCupcake!!.eat()

如果你能够处理 NPE,则!!运算符提供了一个相当方便的功能,即免费智能转换:

val result: String = nullableCupcake!!.eat()

val length: Int = nullableCupcake.eat().length

如果nullableCupcake!!.eat()没有抛出 NPE,Kotlin 将从下一行开始将其类型从Cupcake?更改为Cupcake

Kotlin 的类型系统

类型系统是一组规则,用于确定语言构造的类型。

一个(好的)类型系统将帮助你:

  • 确保程序组成部分以一致的方式连接

  • 理解你的程序(通过减少你的认知负荷)

  • 表达业务规则

  • 自动低级优化

我们已经覆盖了足够的内容来理解 Kotlin 的类型系统。

Any类型

Kotlin 中的所有类型都扩展自Any类型(等等,实际上这不是真的,但为了解释的目的,请耐心等待)。

我们创建的每个类和接口都隐式扩展了Any。因此,如果我们编写一个接受Any作为参数的方法,它将接收任何值:

fun main(args: Array<String>) {

    val myAlmondCupcake = Cupcake.almond()

    val anyMachine = object : Machine<Any> {
      override fun process(product: Any) {
        println(product.toString())
      }
    }

    anyMachine.process(3)

    anyMachine.process("")

    anyMachine.process(myAlmondCupcake)    
}

可空值呢?让我们看看它:

fun main(args: Array<String>) {

    val anyMachine = object : Machine<Any> {
      override fun process(product: Any) {
        println(product.toString())
      }
    }

    val nullableCupcake: Cupcake? = Cupcake.almond()

    anyMachine.process(nullableCupcake) //Error:(32, 24) Kotlin: Type mismatch: inferred type is Cupcake? but Any was expected
}

Any与任何其他类型相同,并且也有一个可空对应物,Any?Any扩展自Any?。因此,最终,Any?是 Kotlin 类型系统层次结构中的顶级类。

最小公共类型

由于类型推断和表达式评估,有时在 Kotlin 中存在一些表达式,其中不清楚返回哪种类型。大多数语言通过返回可能类型选项之间的最小公共类型来解决这个问题。Kotlin 采取了不同的路线。

让我们看看一个模糊表达式的例子:

fun main(args: Array<String>) {
    val nullableCupcake: Cupcake? = Cupcake.almond()

    val length = nullableCupcake?.eat()?.length ?: ""
}

length的类型是什么?Int还是String?不,length值的类型是Any。这很合理。IntString之间的最小公共类型是Any。到目前为止,一切顺利。现在让我们看看以下代码:

val length = nullableCupcake?.eat()?.length ?: 0.0

按照这个逻辑,在这种情况下,length应该有Number类型(IntDouble之间的公共类型),对吧?

错误的,length仍然是Any。Kotlin 在这些情况下不会搜索最小公共类型。如果你想得到一个特定的类型,它必须被显式声明:

val length: Number = nullableCupcake?.eat()?.length ?: 0.0

Unit类型

Kotlin 没有void返回值的方法(就像 Java 或 C 那样)。相反,一个方法(或更准确地说,一个表达式)可以有一个Unit类型。

Unit类型表示表达式被调用是为了其副作用,而不是其返回值。Unit表达式的经典例子是println(),这是一个仅为了其副作用而被调用的方法。

Unit,就像任何其他 Kotlin 类型一样,从Any扩展而来,并且可以是可空的。Unit?看起来很奇怪且不必要,但这是为了与类型系统保持一致性。拥有一致的类型系统有多个优点,包括更好的编译时间和工具支持:

anyMachine.process(Unit)

Nothing 类型

Nothing 是位于整个 Kotlin 层次结构底部的类型。Nothing 继承了所有 Kotlin 类型,包括 Nothing?

但是,为什么我们需要 NothingNothing? 类型?

Nothing 代表一个无法执行的表达式(基本上是抛出异常):

val result: String = nullableCupcake?.eat() ?: throw RuntimeException() // equivalent to nullableCupcake!!.eat()

在 Elvis 操作符的一侧,我们有 String。在另一侧,我们有 Nothing。因为 StringNothing 之间的公共类型是 String(而不是 Any),所以 result 的值是 String

Nothing 对于编译器也有特殊的意义。一旦在表达式中返回 Nothing 类型,之后的行就会被标记为不可达。

Nothing? 是空值的类型:

val x: Nothing? = null

val nullsList: List<Nothing?> = listOf(null)

其他类型

类、接口和对象是面向对象类型系统的良好起点,但 Kotlin 提供了更多构造,例如数据类、注解和枚举(还有一个额外的类型,称为密封类,我们将在后面介绍)。

数据类

在 Kotlin 中创建主要目的是持有数据的类是一种常见模式(在其他语言中也是一种常见模式,例如 JSON 或 Protobuff)。

Kotlin 有一种特定的类用于此目的:

data class Item(val product: BakeryGood,
  val unitPrice: Double,
  val quantity: Int)

要声明数据类,有一些限制:

  • 主要构造函数应至少有一个参数

  • 主要构造函数的参数必须是 valvar

  • 数据类不能是抽象的、开放的、密封的或内部的

在这些限制下,数据类提供了很多好处。

规范方法

规范方法是在 Any 中声明的。因此,Kotlin 中的所有实例都有这些方法。

对于数据类,Kotlin 会为所有规范方法创建正确的实现。

方法如下:

  • equals(other: Any?): Boolean: 此方法比较值等价性,而不是引用。

  • hashCode(): Int: 哈希码是实例的数值表示。当在同一个实例上多次调用 hashCode() 时,它应该始终返回相同的值。当两个实例在 equals 中返回 true 时,它们必须具有相同的 hashCode()

  • toString(): String: 实例的字符串表示。当实例被连接到字符串时,将调用此方法。

copy() 方法

有时,我们希望重用现有实例的值。copy() 方法允许我们创建数据类的新实例,并覆盖我们想要的参数:

val myItem = Item(myAlmondCupcake, 0.40, 5)

val mySecondItem = myItem.copy(product = myCaramelCupcake) //named parameter

在这种情况下,mySecondItemmyItem 复制 unitPricequantity,并替换 product 属性。

析构方法

按照惯例,任何具有一系列名为 component1()component2() 等方法的类实例都可以用于析构声明。

Kotlin 将为任何数据类生成这些方法:

val (prod: BakeryGood, price: Double, qty: Int) = mySecondItem

prod 值使用 component1() 的返回值初始化,price 使用 component2() 的返回值,依此类推。尽管前面的示例使用了显式类型,但这些类型不是必需的:

val (prod, price, qty) = mySecondItem

在某些情况下,并不需要所有值。所有未使用的值都可以用(_)替换:

val (prod, _, qty) = mySecondItem

注解

注解是一种将元信息附加到您的代码上的方式(例如文档、配置等)。

让我们看看以下示例代码:

annotation class Tasty

一个注解本身可以被注解来修改其行为:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Tasty

在这种情况下,Tasty注解可以应用于类、接口和对象,并且可以在运行时查询。

要获取完整的选项列表,请查看 Kotlin 文档。

注解可以有参数,但有一个限制,它们不能为空:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Tasty(val tasty:Boolean = true)

@Tasty(false)
object ElectricOven : Oven {
  override fun process(product: Bakeable) {
    println(product.bake())
  }
}

@Tasty
class CinnamonRoll : Roll("Cinnamon")

@Tasty
interface Fried {
  fun fry(): String
}

要在运行时查询注解值,我们必须使用反射 API(kotlin-reflect.jar必须位于您的类路径中):

fun main(args: Array<String>) {
    val annotations: List<Annotation> = ElectricOven::class.annotations

    for (annotation in annotations) {
        when (annotation) {
            is Tasty -> println("Is it tasty? ${annotation.tasty}")
            else -> println(annotation)
        }
    }
}

枚举

Kotlin 中的枚举是一种定义一组常量值的方式。枚举非常有用,但不仅限于配置值:

enum class Flour {
  WHEAT, CORN, CASSAVA
}

每个元素都是一个扩展了Flour类的对象。

就像任何对象一样,它们可以扩展接口:

interface Exotic {
  fun isExotic(): Boolean
}

enum class Flour : Exotic {
  WHEAT {
    override fun isExotic(): Boolean {
      return false 
    }
  },

  CORN {
    override fun isExotic(): Boolean {
      return false
    }
  },

  CASSAVA {
    override fun isExotic(): Boolean {
      return true
    }
  }
}

枚举也可以有抽象方法:

enum class Flour: Exotic {
  WHEAT {
    override fun isGlutenFree(): Boolean {
      return false
    }

    override fun isExotic(): Boolean {
      return false
    }
  },

  CORN {
    override fun isGlutenFree(): Boolean {
      return true
    }

    override fun isExotic(): Boolean {
      return false
    }
  },

  CASSAVA {
    override fun isGlutenFree(): Boolean {
      return true
    }

    override fun isExotic(): Boolean {
      return true
    }
  };

  abstract fun isGlutenFree(): Boolean
}

任何方法定义都必须在分隔最后一个元素的分号(;)之后声明。

当枚举与when表达式一起使用时,Kotlin 编译器会检查所有情况是否都已覆盖(单独或使用else):

fun flourDescription(flour: Flour): String {
  return when(flour) { // error
    Flour.CASSAVA -> "A very exotic flavour"
  }
}

在这个例子中,我们只检查了CASSAVA,而没有检查其他元素;因此,它失败了:

fun flourDescription(flour: Flour): String {
  return when(flour) {
    Flour.CASSAVA -> "A very exotic flavour"
    else -> "Boring"
  }
}

摘要

在本章中,我们介绍了面向对象编程的基础以及 Kotlin 如何支持它。我们学习了如何使用类、接口、对象、数据类、注解和枚举。我们还探讨了 Kotlin 类型系统,并看到了它是如何帮助我们编写更好、更安全的代码的。

在下一章中,我们将从函数式编程的介绍开始。

第二章:函数式编程入门

函数式编程在过去五年中在软件行业中引起了巨大波澜,每个人都想搭上这趟车。函数式编程的历史要悠久得多,始于 20 世纪 50 年代,Lisp被认为是第一种编程语言(或者至少是第一个引入函数式特性的语言),它仍然以Common Lisp的形式存在,以及其他方言,如SchemeClojure

在本章中,我们将涵盖以下主题:

  • 什么是函数式编程?

  • 基本概念

  • 函数式集合

  • 实现函数式列表

什么是函数式编程?

函数式编程是一种范式(一种组织程序的风格)。本质上,重点是使用表达式转换数据(理想情况下,这些表达式不应有副作用)。其名称“函数式”基于数学函数的概念(而不是子程序、方法或过程)。数学函数定义了一组输入和输出之间的关系。每个输入只有一个输出。例如,给定一个函数,f(x) = X²; f(5) 总是 25

在编程语言中,为了保证调用带有参数的函数总是返回相同的值,需要避免访问可变状态:

fun f(x: Long) : Long { 
   return x * x // no access to external state
}

f 函数不访问任何外部状态;因此,调用 f(5) 总是返回 25

fun main(args: Array<String>) {
    var i = 0

    fun g(x: Long): Long {
       return x * i // accessing mutable state
    }

    println(g(1)) //0
    i++
    println(g(1)) //1
    i++
    println(g(1)) //2
}

另一方面,g 函数依赖于可变状态,并且对于相同的输入返回不同的值。

现在,在现实生活中的程序(一个内容管理系统CMS)、购物车或聊天程序)中,状态会发生变化。因此,在函数式编程风格中,状态管理必须是明确和谨慎的。在后面的章节中,我们将介绍在函数式编程中管理状态变化的技术。

函数式编程风格将为我们提供以下好处:

  • 代码易于阅读和测试:不依赖于外部可变状态的功能更容易推理和证明

  • 状态和副作用是精心规划的:将状态管理限制在我们代码的个体和特定位置,使得维护和重构变得容易

  • 并发性变得更安全且更自然:没有可变状态意味着并发代码在你的代码周围需要更少或没有锁

基本概念

函数式编程由几个定义良好的概念组成。以下是对这些概念的简要介绍,稍后将在下一章中深入探讨每个概念。

一等和高级函数

函数式编程最基础的概念是一等函数。支持一等函数的编程语言会将函数视为任何其他类型;这样的语言将允许你使用函数作为变量、参数、返回值、泛化类型等。说到参数和返回值,使用或返回其他函数的函数是高阶函数

Kotlin 支持这两个概念。

让我们尝试一个简单的函数(在 Kotlin 的文档中这种函数被称为 lambda):

val capitalize = { str: String -> str.capitalize() }

fun main(args: Array<String>) {
   println(capitalize("hello world!"))
}

capitalizelambda 函数的类型是(String) -> String;换句话说,capitalize将接受String并返回另一个String——在这种情况下,一个首字母大写的String

作为 lambda 函数,capitalize可以用带参数的括号(或者完全没有参数,具体取决于情况)来执行。

(String) -> String类型意味着什么呢?

(String) -> String是一个快捷方式(有些人可能称之为语法糖),它是Function1<String, String>的简称,Function1<P1, R>是在 Kotlin 标准库中定义的一个接口。Function1<P1, R>有一个标记为操作符的方法invoke(P1): R(我们稍后会介绍操作符)。

Kotlin 的编译器可以在编译时将快捷语法转换为完整的函数对象(实际上,编译器还会应用更多的优化),如下所示:

val capitalize = { str: String -> str.capitalize() }

它等同于以下代码:

val capitalize = object : Function1<String, String> {
   override fun invoke(p1: String): String {
      return p1.capitalize()
   }
}

正如你所见,capitalize值的主体位于invoke方法内部。

在 Kotlin 中,lambda 函数也可以用作其他函数的参数。

让我们看看以下示例:

fun transform(str:String, fn: (String) -> String): String {
   return fn(str)
}

transform(String, (String) -> String)函数接受一个String并对其应用一个 lambda 函数。

从所有目的来看,我们可以泛化transform

fun <T> transform(t: T, fn: (T) -> T): T {
   return fn(t)
}

使用transform非常简单。看看下面的代码片段:

fun main(args: Array<String>) {
    println(transform("kotlin", capitalize))
}

我们可以直接传递capitalize作为参数,真是太棒了。

调用transform函数的方式有很多。让我们再试几个:

fun reverse(str: String): String {
   return str.reversed()
}

fun main(args: Array<String>) {
    println(transform("kotlin", ::reverse))
}

reverse是一个函数;我们可以使用双冒号(::)来传递它的引用,如下所示:

object MyUtils {
   fun doNothing(str: String): String {
      return str
   }
}

fun main(args: Array<String>) {
    println(transform("kotlin", MyUtils::doNothing))
}

doNothing是一个对象方法,在这种情况下,我们在MyUtils对象名称后使用::

class Transformer {
   fun upperCased(str: String): String {
      return str.toUpperCase()
   }

   companion object {
      fun lowerCased(str: String): String {
         return str.toLowerCase()
      }
   }
}

fun main(args: Array<String>) {
    val transformer = Transformer()

    println(transform("kotlin", transformer::upperCased))

    println(transform("kotlin", Transformer.Companion::lowerCased))
}

我们也可以传递实例或伴生对象的引用。但最常见的情况可能是直接传递一个 lambda 表达式:

fun main(args: Array<String>) {
    println(transform("kotlin", { str -> str.substring(0..1) }))
}

使用it隐式参数的简短版本如下:

fun main(args: Array<String>) {
    println(transform("kotlin", { it.substring(0..1) }))
}

it是一个隐式参数(你不需要显式声明它),它可以用作只有一个参数的 lambda 表达式。

虽然使用it在所有情况下都很诱人,但一旦你开始使用连续或嵌套的 lambda,它们可能会很难阅读。要谨慎使用,并且当它清楚类型时(没有打趣的意思)。

如果一个函数将 lambda 作为最后一个参数接收,lambda 可以放在括号之外传递:

fun main(args: Array<String>) {
    println(transform("kotlin") { str -> str.substring(0..1) })
}

这个特性为使用 Kotlin 创建领域特定语言DSL)打开了可能性。

你知道Ruby中的unless流程控制语句吗?unless是一个在条件为false时执行代码块的控制语句;它有点像否定if条件,但没有else子句。

让我们通过执行以下代码片段来为 Kotlin 创建一个版本:

fun unless(condition: Boolean, block: () -> Unit){
   if (!condition) block()
}

fun main(args: Array<String>) {
    val securityCheck = false // some interesting code here

    unless(securityCheck) {
        println("You can't access this website")
    }
}

unless 接收一个布尔条件作为参数,并以 lambda () -> Unit(无参数和无返回值)的形式阻塞执行。当 unless 执行时,它看起来就像 Kotlin 的任何其他控制流结构。

现在,类型别名可以与函数混合使用,以替换简单的接口。以下是一个例子,来自 第一章,Kotlin – 数据类型、对象和类

interface Machine<T> {
   fun process(product: T)
}

fun <T> useMachine(t: T, machine: Machine<T>) {
   machine.process(t)
}

class PrintMachine<T> : Machine<T> {
   override fun process(t: T) {
      println(t)
   }
}

fun main(args: Array<String>) {
    useMachine(5, PrintMachine())

    useMachine(5, object : Machine<Int> {
       override fun process(t: Int) {
          println(t)
       }
    })
}

它可以用类型别名替换,并使用所有函数的语法特性:

typealias Machine<T> = (T) -> Unit

fun <T> useMachine(t: T, machine: Machine<T>) {
   machine(t)
}

class PrintMachine<T>: Machine<T> {
   override fun invoke(p1: T) {
      println(p1)
   }
} 

fun main(args: Array<String>) {
    useMachine(5, PrintMachine())

    useMachine(5, ::println)

    useMachine(5) { i ->
        println(i)
    }
}

纯函数

纯函数 没有副作用,也没有内存或 I/O。纯函数有许多属性,包括引用透明性、缓存(记忆化)以及其他(我们将在下一章中介绍这些功能)。

在 Kotlin 中可以编写纯函数,但编译器不会像其他语言那样强制执行。是否创建纯函数以享受其好处取决于你。因为 Kotlin 不强制执行纯函数,所以许多程序员说 Kotlin 不是一个真正的函数式编程工具,也许他们是对的。是的,Kotlin 不强制执行纯函数式编程,这为你提供了极大的灵活性,包括如果你愿意,可以以纯函数式风格编写代码。

递归函数

递归函数 是调用自身的函数,具有某种停止执行的条件。在 Kotlin 中,递归函数维护一个栈,但可以使用 tailrec 修饰符进行优化。

让我们看一个例子,一个 阶乘 函数的实现。

首先,让我们看看一个典型的命令式实现,以下代码片段中的循环和状态变化:

fun factorial(n: Long): Long {
   var result = 1L
   for (i in 1..n) {
      result *= i
   }
   return result
}

这没有什么特别之处,也不特别优雅。现在,让我们看看一个递归实现,没有循环,也没有状态变化:

fun functionalFactorial(n: Long): Long {
   fun go(n: Long, acc: Long): Long {
      return if (n <= 0) {
         acc
      } else {
         go(n - 1, n * acc)
      }
   }

   return go(n, 1)
} 

我们使用一个内部递归函数;go 函数在达到条件之前调用自身。正如你所看到的,我们是从最后一个 n 值开始的,并在每次递归迭代中减少它。

优化后的实现类似,但带有 tailrec 修饰符:

fun tailrecFactorial(n: Long): Long {
   tailrec fun go(n: Long, acc: Long): Long {
      return if (n <= 0) {
         acc
      } else {
         go(n - 1, n * acc)
      }
   }

   return go(n, 1)
}

要测试哪个实现更快,我们可以编写一个简陋的分析函数:

fun executionTime(body: () -> Unit): Long {
   val startTime = System.nanoTime()
   body()
   val endTime = System.nanoTime()
   return endTime - startTime
}

对于我们的目的,executionTime 函数是可行的,但任何严肃的生产代码都应该使用适当的分析工具进行性能分析,例如 Java Microbenchmark HarnessJMH):

fun main(args: Array<String>) {
    println("factorial :" + executionTime { factorial(20) })
    println("functionalFactorial :" + executionTime { functionalFactorial(20) })
    println("tailrecFactorial :" + executionTime { tailrecFactorial(20) })
}

以下是前面代码的输出:

图片

tailrec 优化的版本甚至比正常的命令式版本更快。但 tailrec 不是一个魔法咒语,可以使你的代码运行得更快。一般来说,tailrec 优化的代码将比未优化的版本运行得更快,但并不总是能打败好的旧命令式代码。

让我们探索一个斐波那契数列的实现,从一个命令式实现开始如下:

fun fib(n: Long): Long {
   return when (n) {
      0L -> 0
      1L -> 1
      else -> {
         var a = 0L
         var b = 1L
         var c = 0L
         for (i in 2..n) {
            c = a + b
            a = b
            b = c
         }
         c
      }
   }
}

现在,让我们看看一个函数式递归实现:

fun functionalFib(n: Long): Long {
   fun go(n: Long, prev: Long, cur: Long): Long {
      return if (n == 0L) {
         prev
      } else {
         go(n - 1, cur, prev + cur)
      }
   }

   return go(n, 0, 1)
}

现在,让我们检查其对应的 tailrec 版本,如下所示:

fun tailrecFib(n: Long): Long {
   tailrec fun go(n: Long, prev: Long, cur: Long): Long {
      return if (n == 0L) {
         prev
      } else {
         go(n - 1, cur, prev + cur)
      }
   }

   return go(n, 0, 1)
}

然后,再次,让我们用 executionTime 来查看其分析:

fun main(args: Array<String>) {
    println("fib :" + executionTime { fib(93) })
    println("functionalFib :" + executionTime { functionalFib(93) })
    println("tailrecFib :" + executionTime { tailrecFib(93) })
}

输出将类似于以下内容:

截图

tailrec 实现比递归版本快得多,但不如正常命令式实现快。

懒加载

一些函数式语言提供了 lazy(非严格)评估模式。Kotlin 默认使用 贪婪(严格)评估

Kotlin 本身不提供对懒加载的原生支持,但作为 Kotlin 标准库的一部分,以及一个名为 委托属性 的语言特性(我们将在未来的章节中详细讨论):

fun main(args: Array<String>) {
    val i by lazy {
        println("Lazy evaluation")
        1
    }

    println("before using i")
    println(i)
}

输出将类似于以下截图:

内容

by 保留字之后,lazy() 高阶函数接收一个 (() -> T) 初始化 lambda 函数,该函数将在第一次访问 i 时执行。

但也可以使用正常的 lambda 函数来处理一些懒加载用例:

fun main(args: Array<String>) {
    val size = listOf(2 + 1, 3 * 2, 1 / 0, 5 - 4).size
}

如果我们尝试执行这个表达式,它将抛出一个 ArithmeticException 异常,因为我们正在除以零:

fun main(args: Array<String>) {
    val size = listOf({ 2 + 1 }, { 3 * 2 }, { 1 / 0 }, { 5 - 4 }).size
}

执行这个没有问题。出问题的代码没有被执行,实际上使其成为一个 lazy 评估。

功能集合

函数式集合 是那些通过高阶函数提供与其元素交互方式的集合。函数式集合具有名为 filtermapfold 等常见操作;这些名称是通过约定(类似于设计模式)定义的,并在多个库和语言中实现。

不要与纯函数式数据结构混淆——这是在纯函数式语言中实现的数据结构。纯函数式数据结构是不可变的,并使用 lazy 评估和其他函数式技术。

函数式集合可以是,但不一定是纯函数式数据结构。我们已经讨论了算法的命令式实现可以比函数式实现更快。

Kotlin 随带一个优秀的函数式集合库。让我们看看它:

val numbers: List<Int> = listOf(1, 2, 3, 4)

我们的价值 numbers 是一个 List<Int> 类型的值。现在,让我们按照以下方式打印其成员:

fun main(args: Array<String>) {
    for(i in numbers) {
       println("i = $i")
    }
}

到目前为止,一切顺利,但它看起来并不太像函数式。

不必再担心;Kotlin 集合包括许多接收 lambda 来操作其成员的函数。我们可以用 lambda 替换这个循环,如下所示:

fun main(args: Array<String>) {
    numbers.forEach { i -> println("i = $i") }
}

现在,让我们在以下代码中转换我们的集合:

val numbers: List<Int> = listOf(1, 2, 3, 4)

fun main(args: Array<String>) {
    val numbersTwice: List<Int> = listOf()

    for (i in numbers) {
       numbersTwice.add(i * 2) //Compilation error: Unresolved reference: add 
    }
}

这段代码无法编译;numberTwice 没有提供 add(T) 方法。List<T> 是一个不可变列表;一旦初始化,它就可以被修改。要向列表中添加元素,它必须具有不同的类型——在我们的例子中是 MutableList<T>

val numbers: List<Int> = listOf(1, 2, 3, 4)

fun main(args: Array<String>) {
    val numbersTwice: MutableList<Int> = mutableListOf()

    for (i in numbers) {
        numbersTwice.add(i * 2) //Nice!
    }
}

MutableList<T> 扩展了 List<T>;它添加了修改集合本身的方法,例如 add(T)remove(T)clear 以及其他方法。

Kotlin 的所有主要集合类型(List<T>, Set<T>, 和 Map<K, V>)都有可变子类型(MutableList<T>, MutableSet<T>, 和 MutableMap<K, V>)。

但我们可以将这个转换替换为以下代码中的单行表达式:

val numbers: List<Int> = listOf(1, 2, 3, 4)

fun main(args: Array<String>) {
    val numbersTwice: List<Int> = numbers.map { i -> i * 2 }
}

map 操作允许你转换(技术上是对值进行映射)。这段代码有很多优点,并且更加简洁,现在 numbersTwice 的值是一个 List<Int> 列表,而不是 MutableList<T> 列表。

让我们再举几个例子。我们可以使用循环来计算数字的所有元素之和:

val numbers: List<Int> = listOf(1, 2, 3, 4)

fun main(args: Array<String>) {
    var sum = 0

    for (i in numbers) {
       sum += i
    }

    println(sum)    
}

这可以简化为只有一行,使用不可变的 sum 值如下:

val numbers: List<Int> = listOf(1, 2, 3, 4)

fun main(args: Array<String>) {
    val sum = numbers.sum()

    println(sum)    
}

很好,但不是很吸引人,所以让我们提高难度:

val numbers: List<Int> = listOf(1, 2, 3, 4)

fun main(args: Array<String>) {
    val sum = numbers.fold(0) { acc, i -> acc + i }

    println(sum)
}

fold 方法遍历一个集合,保持一个累加器值。fold 接收一个 T 值作为初始值;在第一次迭代中,这个初始值将是累加器,后续迭代将使用 lambda 的返回值作为下一个累加器值:

val numbers: List<Int> = listOf(1, 2, 3, 4)

fun main(args: Array<String>) {
    val sum = numbers.fold(0) { acc, i ->
        println("acc, i = $acc, $i")
        acc + i
    }

    println(sum)
}

输出将类似于以下截图:

截图

fold 类似,reduce 遍历一个集合,有一个累加器但没有初始值:

val numbers: List<Int> = listOf(1, 2, 3, 4)

fun main(args: Array<String>) {
    val sum = numbers.reduce { acc, i ->
        println("acc, i = $acc, $i")
        acc + i
    }

    println(sum)
}

输出将类似于以下截图:

截图

foldreduce 有对应的 foldRightreduceRight,它们从最后一个项目开始迭代到第一个项目。

实现一个函数式列表

在前两章学到的所有知识的基础上,我们可以实现一个纯函数式列表:

sealed class FunList<out T> {
   object Nil : FunList<Nothing>()

   data class Cons<out T>(val head: T, val tail: FunList<T>) : FunList<T>()
}

FunList 类是一个密封类;只有两个可能的子类存在——Nil,一个空列表(在其他书中你可以看到它被定义为 NullEmpty)和 Cons(一个结构,名称来自 Lisp,它包含两个值)。

T 类型被标记为 out;这是为了变异性,我们将在未来的章节中介绍变异性。

Nil 是一个对象(我们不需要 Nil 的不同实例)扩展 FunList<Nothing>(记住 Nothing 是 Kotlin 类型层次结构的底部)。

Cons 值包含两个值——head,一个单独的 T,和 tail,一个 FunList<T>;因此,它可以是一个 Nil 值或另一个 Cons

让我们创建一个列表实例如下:

import com.packtpub.functionalkotlin.chapter02.FunList.Cons
import com.packtpub.functionalkotlin.chapter02.FunList.Nil

fun main(args: Array<String>) {
    val numbers = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
}

它是函数式的,但不是很易读。我们可以创建一个更好的初始化函数:

import com.packtpub.functionalkotlin.chapter02.FunList.Cons
import com.packtpub.functionalkotlin.chapter02.FunList.Nil

fun intListOf(vararg numbers: Int): FunList<Int> {
   return if (numbers.isEmpty()) {
      Nil
   } else {
      Cons(numbers.first(), intListOf(*numbers.drop(1).toTypedArray().toIntArray()))
   }
}

这里有很多新东西。参数 numbers 被标记为 vararg,这意味着我们可以用尽可能多的参数调用这个函数。从所有目的来看,numbers 是一个 IntArray 值(一种特殊的数组类型)。如果 numbers 为空,我们可以返回 Nil。如果不为空,我们可以提取第一个元素作为我们的 head 值,并递归调用 intListOf 来获取 tail 值。为了提取 tail 值,我们使用 drop 方法并将结果转换为 IntArray 值。但我们不能直接将任何数组作为 vararg 传递;因此,我们必须使用展开(*)运算符来逐个传递数组的每个成员。

现在,我们可以创建我们的 FunList<Int> 值:

fun main(args: Array<String>) {
    val numbers = intListOf(1, 2, 3, 4)    
}

让我们按照以下方式实现 forEach

sealed class FunList<out T> {
   object Nil : FunList<Nothing>()

   data class Cons<out T>(val head: T, val tail: FunList<T>) : FunList<T>()

   fun forEach(f: (T) -> Unit) {
      tailrec fun go(list: FunList<T>, f: (T) -> Unit) {
         when (list) {
            is Cons -> {
               f(list.head)
               go(list.tail, f)
            }
            is Nil -> Unit//Do nothing
         }
      }

      go(this, f)
   }

}

forEach 的实现类似于我们在递归部分中的阶乘和斐波那契函数的例子,包括 tailrec

从技术上来说,FunList 是一个 代数数据类型ADT)。FunList 可以是 NilCons,除此之外没有其他可能。Kotlin 的编译器可以使用这些信息来检查当 FunList 类型用作 when 控制结构中的参数时,两个值是否都被评估:

fun main(args: Array<String>) {
    val numbers = intListOf(1, 2, 3, 4)

    numbers.forEach { i -> println("i = $i") }
}

实现 fold 将类似于以下代码:

sealed class FunList<out T> {

  /*Previous code here*/

   fun <R> fold(init: R, f: (R, T) -> R): R {
      tailrec fun go(list: FunList<T>, init: R, f: (R, T) -> R): R = when (list) {
         is Cons -> go(list.tail, f(init, list.head), f)
         is Nil -> init
      }

      return go(this, init, f)
   }
}

你注意到这些函数实现起来非常简单吗?让我们看看下面的代码:

fun main(args: Array<String>) {
    val numbers = intListOf(1, 2, 3, 4)

    val sum = numbers.fold(0) { acc, i -> acc + i}
}

那么,Kotlin 的列表和我们的函数式列表之间来一场小比赛如何?

fun main(args: Array<String>) {
    val funList = intListOf(1, 2, 3, 4)
    val list = listOf(1, 2, 3, 4)

    println("fold on funList : ${executionTime { funList.fold(0) { acc, i -> acc + i } }}")
    println("fold on list : ${executionTime { list.fold(0) { acc, i -> acc + i } }}")
}

输出将类似于以下截图:

图片

哎呀!我们的实现速度慢了 10 倍。别担心,Kotlin 的实现是一个高度优化的命令式解决方案,而我们的只是为了学习和娱乐(当然,是字面意义上的娱乐)。

那么 map 呢?要在函数式编程中实现 map,我们需要先实现其他函数。让我们从 reverse 开始。

reverse 是一个返回反转顺序列表的函数:

sealed class FunList<out T> {

    /*previous code*/

    fun reverse(): FunList<T> = fold(Nil as FunList<T>) { acc, i -> Cons(i, acc) }
}

我们可以在每次迭代中重用 fold 并构建一个新的 Cons 值,使用 acc 值作为 tail。这是函数式编程的一个大优点——重用现有函数。

现在,我们可以实现 foldRight

sealed class FunList<out T> {

    /*previous code*/

  fun <R> foldRight(init: R, f: (R, T) -> R): R {
   return this.reverse().fold(init, f)
  }
}

再次强调,我们正在重用现有函数。现在是时候实现我们的 map 函数了。在这个阶段,我们重用现有函数并不令人惊讶:

sealed class FunList<out T> {

 /*previous code*

 fun <R> map(f:(T) -> R): FunList<R> {
   return foldRight(Nil as FunList<R>){ tail, head -> Cons(f(head), tail) }
 }
}

foldRight 是我们所需要的全部。正如你所见,我们可以使用函数和其他基本概念作为构建块来实现一个完整的列表。这就是函数式编程的全部内容。

概述

在本章中,我们介绍了函数式编程的基础,包括高阶函数、纯函数、递归函数和惰性求值。我们还介绍了函数式集合,并使用函数式编程风格实现了一个函数式集合。

在下一章中,我们将介绍函数式编程的基础——不可变性。

第三章:不可变性 - 它很重要

因此,我们现在进入了《函数式 Kotlin》的第三章。在本章中,我们将讨论不可变性。不可变性可能是函数式编程最重要的方面;实际上,不仅在函数式编程中,面向对象编程也通过不可变对象为培养不可变性留出了一些空间。那么,为什么它如此重要?这意味着什么?我们如何在 Kotlin 中实现不可变性?让我们在本章中回答这些问题。

以下是我们将在本章中讨论的要点:

  • 什么是不可变性?

  • 不可变性的优点

  • Kotlin 中如何实现不可变性?

  • 变量中的不可变性

  • valvar

  • valconst val——它们真的是不可变的吗?

  • 编译器优化

  • 不可变集合

  • 不可变性的缺点

什么是不可变性?

函数式编程由于其本质是线程安全的;不可变性在使其线程安全方面起着重要作用。如果你按照词典的定义,不可变性意味着某物是不可改变的。所以,按照词典,一个不可变变量是一个不能改变的变量。那么,这如何有助于线程安全?

以下示例展示了一个简单的类,没有额外的线程安全保护措施:

class MutableObject { 
    var mutableProperty:Int = 1 
} 

想象一下同时从多个线程调用这个类的情况。这里没有保证完整性,对吧?

现在,假设将 mutableProperty 变为不可变的;问题部分解决了,对吧?

然而,如果你认为不可变性就是创建一个类并使其所有变量为只读,那么这样的简化解释不仅错误,而且糟糕。实际上,不可变性不是关于禁止改变,而是关于处理改变。不是直接改变属性的底层值,而是创建一个新的属性,并应用更改后复制值。这适用于 Kotlin 和 Java(甚至 C)中的原始数据类型。例如,在以下示例中,当我们编写 var y = x.capitalize() 时,x 的值保持不变,而是将应用更改后的 x 值复制到 y

fun main(args: Array<String>) { 
    var x:String = "abc" 
    var y = x.capitalize() 
    println("x = $x, y = $y") 
} 

大多数原始类型都以相同的方式操作;这就是所谓的不可变性。现在,让我们看看如何在 Kotlin 中实现不可变性,然后我们将探讨其优点和缺点。

在 Kotlin 中实现不可变性

与 Clojure、Haskell、F# 等语言不同,Kotlin 不是一个纯函数式编程语言,其中不可变性是强制性的;相反,我们可以将 Kotlin 称为函数式编程和面向对象语言完美融合的典范。它包含了两个世界的重大好处。因此,与纯函数式编程语言强制不可变性不同,Kotlin 鼓励不可变性,并在可能的情况下自动优先考虑它。

换句话说,Kotlin 有不可变变量(val),但没有保证状态真正深度不可变的语言机制。如果一个 val 变量引用了一个可变对象,其内容仍然可以被修改。我们将对此进行更详细的讨论和深入探讨,但首先让我们看看如何在 Kotlin 中实现引用不可变性和 varvalconst val 之间的区别。

通过真正的状态深度不可变性,我们指的是一个属性在每次被调用时都会返回相同的值,并且该属性永远不会改变其值;如果我们有一个具有自定义获取器的 val 属性,我们就可以轻松避免这种情况。更多详细信息,请参阅以下链接:artemzin.com/blog/kotlin-val-does-not-mean-immutable-it-just-means-readonly-yeah/

varval 的区别

因此,为了鼓励不可变性,同时仍然让开发者有选择权,Kotlin 引入了两种类型的变量。第一种是 var,它只是一个简单的变量,就像在任何命令式语言中一样。另一方面,val 让我们更接近不可变性;再次强调,它并不保证不可变性。那么,val 变量究竟为我们提供了什么?它强制只读,初始化后你不能写入 val 变量。所以,如果你使用没有自定义获取器的 val 变量,你可以实现引用不可变性。

让我们看看;以下程序将无法编译:

fun main(args: Array<String>) { 
    val x:String = "Kotlin" 
    x+="Immutable"//(1) 
} 

如我之前提到的,前面的程序将无法编译;它将在注释 (1) 处产生错误。因为我们已将变量 x 声明为 val,所以 x 将是只读的,一旦我们初始化 x,之后就不能修改它。

所以,你现在可能想知道为什么我们不能用 val 保证不可变性?让我们通过以下示例来检查:

object MutableVal { 
    var count = 0 
    val myString:String = "Mutable" 
        get() {//(1) 
            return "$field ${++count}"//(2) 
        } 
} 

fun main(args: Array<String>) { 
    println("Calling 1st time ${MutableVal.myString}") 
    println("Calling 2nd time ${MutableVal.myString}") 
    println("Calling 3rd time ${MutableVal.myString}")//(3) 
} 

在这个程序中,我们将 myString 声明为一个 val 属性,但实现了一个自定义的 get 函数,我们在返回之前修改了 myString 的值。首先看看输出,然后我们将进一步查看程序:

如你所见,尽管 myString 属性是 val,但它每次被访问时都返回不同的值。现在,让我们查看代码以了解这种行为。

在注释 (1) 中,我们为 val 属性 myString 声明了一个自定义获取器。在注释 (2) 中,我们预增加了 count 的值,并将其添加到 field 值,即 myString 的值之后,并从获取器返回相同的值。所以,每次我们请求 myString 属性时,count 都会增加,在下一次请求时,我们得到不同的值。因此,我们破坏了 val 属性的不可变行为。

编译时常量

那么,我们如何克服这个问题?如何强制不可变性?const val属性就是为了帮助我们而存在的。只需将val myString修改为const val myString,您就不能实现自定义获取器。

虽然val属性是只读变量,但另一方面,const val是编译时常量。您不能将函数的结果(结果)分配给const val。让我们讨论一下valconst val之间的一些区别:

  • val属性是只读变量,而const val是编译时常量

  • val属性可以有自定义获取器,但const val不能

  • 我们可以在 Kotlin 代码的任何地方拥有val属性,在函数内部,作为类成员,任何地方,但const val必须是一个类/对象的顶层成员

  • 您不能为const val属性编写委托

  • 我们可以拥有任何类型的val属性,无论是我们的自定义类还是任何原始数据类型,但只有原始数据类型和String可以使用const val属性

  • 我们不能在const val属性中使用可空数据类型;因此,const val属性也不能有 null 值

因此,const val属性保证了值的不可变性,但灵活性较低,并且您必须仅使用原始数据类型与const val一起使用,这并不总是能满足我们的需求。

现在,我已经多次使用了“引用不可变性”这个词,让我们现在检查它的含义以及有多少种不可变性类型。

不可变性的类型

基本上有以下两种不可变性类型:

  • 引用不可变性

  • 不可变值

不可变引用(引用不可变性)

引用不可变性强制规定,一旦分配了引用,就不能将其分配给其他对象。想想看,就像它是一个自定义类的val属性,甚至是MutableListMutableMap;初始化属性后,您不能从该属性引用其他对象,除非是对象的基本值。例如,考虑以下程序:

class MutableObj { 
    var value = "" 

    override fun toString(): String { 
        return "MutableObj(value='$value')" 
    } 
} 

fun main(args: Array<String>) { 
    val mutableObj:MutableObj = MutableObj()//(1) 
    println("MutableObj $mutableObj") 
    mutableObj.value = "Changed"//(2) 
    println("MutableObj $mutableObj") 

    val list = mutableListOf("a","b","c","d","e")//(3) 
    println(list) 
    list.add("f")//(4) 
    println(list) 
} 

在我们解释程序之前,先看看输出结果:

图片

因此,在这个程序中,我们有两个val属性——listmutableObj。我们使用MutableObj的默认构造函数初始化mutableObj,因为它是一个val属性,它将始终引用那个特定的对象;但是,如果您关注注释(2),我们已经改变了mutableObjvalue属性,因为MutableObj类的value属性是可变的(var)。

list 属性类似,我们可以在初始化后向列表中添加项目,改变其底层值。listmutableObj 都是不可变引用的完美例子;一旦初始化,属性就不能被分配给其他东西,但它们的底层值可以改变(你可以参考输出)。背后的原因是分配给这些属性的 数据类型。MutableObj 类和 MutableList<String> 数据结构本身都是可变的,因此我们无法限制其实例的值变化。

不可变值

另一方面,不可变值同样不允许对值进行更改;维护起来非常复杂。在 Kotlin 中,const val 属性强制值的不可变性,但它们缺乏灵活性(我们已讨论过它们)并且你只能使用原始类型,这在实际场景中可能会带来麻烦。

不可变集合

Kotlin 在可能的情况下会优先考虑不可变性,但将选择权留给开发者,决定是否以及何时使用它。这种选择权使语言更加强大。与大多数语言不同,它们要么只有可变集合(如 Java、C# 等),要么只有不可变集合(如 F#、Haskell、Clojure 等),Kotlin 两者都有,并且区分它们,让开发者有选择使用不可变或可变集合的自由。

Kotlin 为集合对象提供了两个接口——Collection<out E>MutableCollection<out E>;所有集合类(例如 ListSetMap)都实现了这两个接口之一。正如其名所示,这两个接口是为不可变和可变集合分别设计的。让我们举一个例子:

fun main(args: Array<String>) { 
    val immutableList = listOf(1,2,3,4,5,6,7)//(1) 
    println("Immutable List $immutableList") 
    val mutableList:MutableList<Int> = immutableList.toMutableList()//(2) 
    println("Mutable List $mutableList") 
    mutableList.add(8)//(3) 
    println("Mutable List after add $mutableList") 
    println("Mutable List after add $immutableList") 
} 

输出如下:

图片

因此,在这个程序中,我们通过 Kotlin 的 listOf 方法创建了一个不可变列表,注释(1)中提到。listOf 方法使用传递给它的元素(可变参数)创建一个不可变列表。此方法还有一个泛型类型参数,如果元素数组不为空,则可以省略。listOf 方法还有一个可变版本——mutableListOf(),除了返回 MutableList 而外,其余都相同。我们可以使用 toMutableList() 扩展函数将不可变列表转换为可变列表,注释(2)中我们就是这样做的,以便在注释(3)中向其添加一个元素。然而,如果你检查输出,原始的 Immutable List 没有任何变化,而项目已经被添加到了新创建的 MutableList 中。

不可变性的优势

我们已经多次提到,不可变性带来了安全性,但这并非全部;以下是一个简短的列表,列出了不可变性带来的优势,我们将逐一讨论:

  • 线程安全

  • 低耦合

  • 引用透明性

  • 失败原子性

  • 编译器优化

  • 纯函数

让我们逐一讨论这些优点,以便更好地理解它们。

线程安全

我们可能已经看到过一千次不可变性如何带来线程安全。这实际上意味着什么?不可变性是如何实现线程安全的?与多个线程一起工作本身就是一项复杂的工作。当你从多个线程访问一个类时,你需要确保某些事情,比如锁定和释放对象以及同步,但如果你从多个线程访问任何不可变数据,则不需要这些。

感到困惑?让我们用一个关于线程和可变数据的例子来说明:

class MyData { 
    var someData:Int = 0 
} 

fun main(args: Array<String>) { 
    val myData:MyData = MyData() 

    async(CommonPool) { 
        for(i in 11..20) { 
            myData.someData+=i 
            println("someData from 1st async ${myData.someData}") 
            delay(500) 
        } 
    } 

    async(CommonPool) { 
        for(i in 1..10) { 
            myData.someData++ 
            println("someData from 2nd async ${myData.someData}") 
            delay(300) 
        } 
    } 

    runBlocking { delay(10000) } 
} 

在这个程序中,我们使用了两个协程(我们将在第七章中详细讨论协程,使用协程的异步处理),它们在相同的数据上工作。让我们看看以下输出,然后我们将描述和讨论这个程序中的问题:

图片

因此,仔细观察输出。由于两个协程同时在工作myData.someData上,数据一致性在任何一个中都没有得到保证。

解决这个问题的传统方法是使用锁定-释放技术和同步,但这样你也需要为它编写大量的代码,并且为了避免在实现锁定和释放数据时出现死锁。

函数式编程通过不可变性提供了一个一站式解决方案来解决这个问题。让我们看看不可变性和局部变量如何在多线程中拯救你:

class MyDataImmutable { 
    val someData:Int = 0 
} 

fun main(args: Array<String>) { 
    val myData: MyDataImmutable = MyDataImmutable() 

    async(CommonPool) { 
        var someDataCopy = myData.someData 
        for (i in 11..20) { 
            someDataCopy += i 
            println("someData from 1st async $someDataCopy") 
            delay(500) 
        } 
    } 

    async(CommonPool) { 
        var someDataCopy = myData.someData 
        for (i in 1..10) { 
            someDataCopy++ 
            println("someData from 2nd async $someDataCopy") 
            delay(300) 
        } 
    } 

    runBlocking { delay(10000) } 
} 

我们修改了之前的程序,将someData设置为不可变(因为我们不使用这个变量的自定义获取器,所以它将保持不可变)并在两个协程内部使用了局部变量。

看看以下输出;它清楚地表明问题已解决:

图片

低耦合

线程之间的代码依赖性被称为耦合。我们应该尽量降低耦合度,以避免复杂性并使代码库易于阅读和维护。现在,这实际上意味着什么?请参考我们访问和修改someData值的程序,该程序使用了线程。这可以被称为耦合,因为两个线程都相互依赖。为了您的参考,我们复制了以下代码片段:

async(CommonPool) { 
        for(i in 11..20) { 
            myData.someData+=i 
            println("someData from 1st async ${myData.someData}") 
            delay(500) 
        } 
    } 

    async(CommonPool) { 
        for(i in 1..10) { 
            myData.someData++ 
            println("someData from 2nd async ${myData.someData}") 
            delay(300) 
        } 
    } 

在下一个程序中,我们引入了不可变性,耦合度降低了。在这里,两个线程都在读取相同的元素,但一个线程的操作和更改没有影响到另一个线程。

引用透明性

引用透明性的概念表明,一个表达式总是评估为相同的值,无论上下文或任何其他变化。更具体地说,你可以用一个函数的返回值来替换该函数。

通过纯函数的帮助,不可变性可以建立引用透明性。引用透明性强烈否认数据的可变状态。

失败原子性

在传统编程中,一个线程的失败很容易影响另一个线程。由于不可变性强制低耦合,即使我们在任何模块/线程上有异常,应用程序的内部状态也将保持一致。

原因很简单,不可变对象永远不会改变状态。因此,即使某个部分/模块/线程发生故障,它就会停止,并且没有机会传播到应用程序的其他部分。

缓存

由于不可变对象不会改变,它们可以很容易地缓存以提高性能。因此,你可以轻松避免对同一函数/变量进行多次调用,而是将其本地缓存,从而节省大量处理时间。以下是一些缓存的优势:

  • 它减少了服务器资源的开销

  • 通过提供缓存的输出,它提高了应用程序的性能

  • 通过在内存中持久化数据,它减少了从数据库获取数据的 CPU 循环次数

  • 它增加了可靠性

编译器优化

不可变性和引用透明性有助于编译器执行广泛的优化,取代了手动优化代码的需要,并使程序员从这种权衡中解脱出来。

例如,当你使用编译时常量(const val)时,这适用,因为编译器知道这些变量的值永远不会改变。

纯函数

通过使用不可变性,我们可能得到的最大礼物是纯函数(下一章将介绍)。基本上,纯函数和不可变性不仅是伴侣,而且是互补的。

没有不可变性,我们无法实现纯函数,而没有纯函数,不可变性也不完整。

因此,既然我们已经了解了不可变性和其优点,那么现在让我们关注另一方面:不可变性的缺点,并检查它们是否真的是缺点。

不可变性的缺点

世界上没有只带来好处的东西。

有一个伟大的谚语:一切皆有其价。

你能听到的唯一反对不可变性的说法是,每次你想修改它时都需要创建一个新的对象。在某些情况下这是真的,尤其是在你处理大量对象时。然而,当你处理小数据集或对象时,它没有任何影响。

摘要

在本章中,我们学习了不可变性和如何使用 Kotlin 实现不可变性。我们了解到 Kotlin 为我们提供了根据需求选择不可变对象或可变对象的自由。我们不仅讨论了不可变性的优点,还讨论了其局限性。

下一章将重点介绍函数、函数类型和副作用。我们还将学习纯函数,它们不仅是不可变性的伴侣,而且是不可变性的补充部分,将在下一章中介绍。

那你还在等什么?现在就翻页吧。

第四章:函数、函数类型和副作用

函数式编程围绕不可变性和函数的概念展开。我们在上一章学习了不可变性;在讨论不可变性时,我们也对纯函数有了一定的了解。纯函数基本上是函数式编程提供的许多类型(但可能是最重要的一种)之一。

本章将围绕函数展开。要深入了解函数式编程,你需要对函数有坚实的基础。为了使你的概念清晰,我们将从普通的 Kotlin 函数开始,然后逐步讨论函数式编程定义的抽象函数概念。我们还将看到它们在 Kotlin 中的实现。

在本章中,我们将涵盖以下主题:

  • Kotlin 中的函数

  • 函数类型

  • Lambda

  • 高阶函数

  • 理解副作用和纯函数

因此,让我们从定义函数开始。

Kotlin 中的函数

函数是编程中最重要的一部分。我们每周都要为我们的项目编写大量的函数。函数也是编程基础的一部分。为了学习函数式编程,我们必须对函数的概念有清晰的认识。在本节中,我们将介绍函数的基础知识,以便让你为下一节做好准备,在下一节中,我们将讨论抽象的函数概念及其在 Kotlin 中的实现。

因此,让我们先定义函数。

函数是一块组织良好、可重用的代码块,用于执行单个、相关的操作。

不太清楚?我们将解释,但首先,让我们了解为什么我们应该编写函数。简而言之,函数的功能是什么?看看:

  • 函数允许我们将程序分解成一系列步骤和子步骤

  • 函数鼓励代码重用

  • 如果正确使用,函数可以帮助我们保持代码的整洁、有序和易于理解

  • 函数使测试(单元测试)变得容易,测试程序的小部分比一次性测试整个程序要容易

在 Kotlin 中,函数通常看起来像以下这样:

fun appropriateFunctionName(parameter1:DataType1, parameter2:DataType2,...): ReturnType { 
    //do your stuff here 
    return returnTypeObject 
} 

在 Kotlin 中,函数声明以 fun 关键字开头,后跟函数名,然后是括号。在括号内,我们可以指定函数参数(可选)。括号之后,会有一个冒号(:)和返回类型,它指定了要返回的值/对象的类型(如果你不打算从函数中返回任何内容,可以省略返回类型;在这种情况下,函数将被分配默认返回类型 Unit)。之后,会有函数体,用花括号括起来(对于单表达式函数,花括号是可选的,将在第五章中介绍,更多关于函数的内容)。

Unit 是 Kotlin 中的一个数据类型。Unit 是其自身的单例实例,并持有 Unit 自身的值。Unit 对应于 Java 中的 void,但它与 void 非常不同。虽然 void 在 Java 中意味着没有东西,且 void 不能包含任何东西,但在 Kotlin 中我们有 Nothing 用于此目的,它表示函数永远不会成功完成(由于异常或无限循环)。

现在,那些返回类型、参数(参数)和函数体是什么?让我们来探索它们。

以下是一个比之前展示的抽象函数更现实的函数示例:

fun add(a:int, b:Int):Int { 
   val result = a+b 
   return result 
} 

现在,让我们看看以下关于函数各部分的解释:

  • 函数参数/参数:这些是函数要处理的数据(除非是 lambda)。在我们的例子中,ab 是函数参数。

  • 函数体:我们写在函数花括号内的所有内容都称为 函数体。它是函数的一部分,我们在其中编写逻辑或指令集以完成特定任务。在前面的例子中,花括号内的两行是函数体。

  • 返回语句,数据类型:如果我们愿意从函数中返回某个值,我们必须声明我们愿意返回的值的类型;这个类型被称为 return 类型——在这种情况下,Intreturn 类型,return result 是返回语句,它使您能够向调用函数返回一个值。

我们可以通过删除 val result = a+b 并用 return a+b 替换返回语句来使前面的例子更短。在 Kotlin 中,我们可以进一步缩短这个例子,正如我们将在第五章 更多关于函数 中看到的。

虽然编写函数很容易,但 Kotlin 使之更加容易。

Kotlin 将各种功能捆绑到函数中,使开发者的生活更加轻松。以下是 Kotlin 捆绑的特性的简要列表:

  • 单表达式函数

  • 扩展函数

  • 内联函数

  • 中缀表示法及其他

我们将在第五章 更多关于函数 的 Lambda,泛型,递归,核心递归 部分详细介绍它们。

从函数中返回两个值

虽然,通常函数只能返回单个值,但在 Kotlin 中,通过利用 Pair 类型的优势和结构化声明的优势,我们可以从函数中返回两个变量。考虑以下示例:

fun getUser():Pair<Int,String> {//(1) 
    return Pair(1,"Rivu") 
} 
fun main(args: Array<String>) { 
    val (userID,userName) = getUser()//(2) 
     println("User ID: $userID t User Name: $userName") 
} 

在前面的程序中,在注释 (1) 处,我们创建了一个将返回 Pair<Int,String> 值的函数。

在注释(2)中,我们以似乎返回两个变量的方式使用了该函数。实际上,解构声明允许你解构一个data class/Pair并获取其底层值作为独立的变量。当这个特性与函数一起使用时,它似乎函数返回多个值,尽管它只返回一个值,这个值是一个Pair值或另一个data class

扩展函数

Kotlin 为我们提供了扩展函数。它们是什么?它们是在现有数据类型/类之上的一种临时函数。

例如,如果我们想计算字符串中的单词数,以下是一个传统的函数来完成这个任务:

fun countWords(text:String):Int { 
    return text.trim() 
            .split(Pattern.compile("\s+")) 
            .size 
} 

我们会将一个String传递给一个函数,让我们的逻辑计算单词数,然后我们会返回这个值。

但你不觉得如果有一种方法可以直接在String实例上调用这个函数会更好吗?Kotlin 允许我们执行这样的操作。

看看以下程序:

fun String.countWords():Int { 
    return trim() 
            .split(Pattern.compile("\s+")) 
            .size 
} 

仔细看看函数声明。我们声明了函数为String.countWords(),而不是像之前那样只是countWords;这意味着现在应该在一个String实例上调用它,就像String类的成员函数一样。就像以下代码:

fun main(args: Array<String>) { 
    val counts = "This is an example StringnWith multiple words".countWords() 
    println("Count Words: $counts") 
} 

你可以查看以下输出:

默认参数

我们可能有一个要求,即希望函数有一个可选参数。考虑以下示例:

fun Int.isGreaterThan(anotherNumber:Int):Boolean { 
    return this>anotherNumber 
} 

我们希望anotherNumber参数是可选的;如果我们没有将其作为参数传递,我们希望它是0。传统的方法是有一个没有参数的重载函数,它会用0调用这个函数,如下所示:

fun Int.isGreaterThan(anotherNumber:Int):Boolean { 
    return this>anotherNumber 
} 
fun Int.isGreaterThan():Boolean { 
    return this.isGreaterThan(0) 
} 

然而,在 Kotlin 中,事情相当简单直接,并且它们不需要我们再次定义函数来仅使参数可选。为了使参数可选,Kotlin 为我们提供了默认参数,通过它我们可以在声明函数时立即指定默认值。

以下是被修改后的函数:

fun Int.isGreaterThan(anotherNumber:Int=0):Boolean { 
    return this>anotherNumber 
} 

我们会使用main函数如下:

fun main(args: Array<String>) { 
    println("5>0: ${5.isGreaterThan()}") 
    println("5>6: ${5.isGreaterThan(6)}") 
} 

对于第一个,我们跳过了参数,对于第二个,我们提供了6。所以,对于第一个,输出应该是 true(因为5确实大于0),而对于第二个,它应该是 false(因为5不大于6)。

以下截图输出确认了相同的结果:

嵌套函数

Kotlin 允许你在函数内部嵌套函数,我们可以在另一个函数内部声明和使用函数。

当你在另一个函数内部声明一个函数时,嵌套函数的可见性将仅限于父函数,并且不能从外部访问。

因此,让我们举一个例子:

fun main(args: Array<String>) { 
    fun nested():String { 
        return "String from nested function" 
    } 
    println("Nested Output: ${nested()}") 
} 

在前面的程序中,我们在main函数内部声明并使用了一个函数——nested()

以下是你好奇的输出:

因此,我们在函数的基本知识上已经做好准备,让我们继续学习函数式编程。在下一节中,我们将学习函数类型。

函数式编程中的函数类型

函数式编程的主要目标之一是实现模块化编程。副作用(一个将在本章后面定义的功能性术语)通常是 bug 的来源;函数式编程希望你能完全避免副作用。

为了实现这一点,函数式编程定义了以下类型的函数:

  • Lambda 函数作为属性

  • 高阶函数

  • 纯函数

  • 部分函数

在本节中,我们将按顺序讨论这些概念,以便对函数式编程范式有一个牢固的把握。

那么,让我们从 lambda 开始吧。

Lambda

Lambda,也可以称为匿名函数,在 Kotlin 中具有一等公民的支持。而 Java 中,lambda 的支持是从 Java 8 开始的,在 Kotlin 中,你可以从 JVM 6 开始使用 Kotlin,因此在 Kotlin 中实际上没有 lambda 的障碍。

现在,我们正在谈论 lambda、匿名类(或对象)和匿名函数,但它们究竟是什么呢?让我们来探索一下。

为了通用,lambda 或 lambda 表达式通常指的是匿名函数,即没有名称的函数,可以被赋值给变量、作为参数传递或从另一个函数返回。它是一种嵌套函数,但更灵活、更灵活。你还可以说所有的 lambda 表达式都是函数,但并非所有函数都是 lambda 表达式。匿名和无名称给 lambda 表达式带来了很多好处,我们将在下面讨论。

如我之前提到的,并非所有语言都支持 lambda,Kotlin 是其中最罕见的一种语言,它为 lambda 提供了广泛的支持。

那么,为什么叫 lambda 呢?现在让我们挖掘一点历史吧。

Lambda,Λ,λ(大写Λ,小写λ)是希腊字母的第 11 个字母。发音:lám(b)da。

来源:en.wikipedia.org/wiki/Lambda

在 20 世纪 30 年代,当时在普林斯顿大学学习数学的 Alonzo Church,使用希腊字母,特别是 lambda,来表示他所称的函数。需要注意的是,当时计算机中只有匿名函数;现代命名函数的概念尚未出现。

因此,随着 Alonzo Church 的这种实践,lambda 这个词就与匿名函数(那时唯一的函数类型)联系在了一起,时至今日,它仍然以同样的方式被引用。

阿隆佐·丘奇(1903 年 6 月 14 日-1995 年 8 月 11 日),是一位美国数学家和逻辑学家,他对数学逻辑和理论计算机科学的基础做出了重大贡献。他最著名的是λ演算、丘奇-图灵猜想、证明决定问题的不可判定性、弗雷格-丘奇本体论和丘奇-罗素定理。他还从事语言哲学的研究(例如,丘奇,1970 年)。

来源:en.wikipedia.org/wiki/Alonzo_Church

你不觉得我们已经对理论足够了吗?我们不应该现在专注于学习 lambda 实际上是什么,或者它究竟看起来像什么吗?我们将查看 Kotlin 中 lambda 的样子,但我们更愿意先向您介绍 Java 中的 lambda,然后在 Kotlin 中介绍,以便您完全理解 lambda 在 Kotlin 中拥有多大的力量,以及“一等公民”支持的确切含义。您还将了解 Java 和 Kotlin 中 lambda 的区别。

考虑以下 Java 示例。这是一个简单的例子,其中我们将接口的一个实例传递给一个方法,并在该方法中调用该实例的方法:

public class LambdaIntroClass { 
    interface SomeInterface { 
        void doSomeStuff(); 
    } 
    private static void invokeSomeStuff(SomeInterface someInterface) { 
        someInterface.doSomeStuff(); 
    } 
    public static void main(String[] args) { 
        invokeSomeStuff(new SomeInterface() { 
            @Override 
            public void doSomeStuff() { 
                System.out.println("doSomeStuff invoked"); 
            } 
        }); 
    } 
} 

因此,在这个程序中,SomeInterface是一个接口(LambdaIntroClass的内部接口),只有一个方法——doSomeStuff()。静态方法invokeSomeStuff(为了使它可以通过main方法轻松访问而设置为静态)接受SomeInterface的一个实例,并调用其doSomeStuff()方法。

这只是一个简单的例子;现在,让我们让它更简单:让我们给它添加 lambda 表达式。看看以下更新的代码:

public class LambdaIntroClass { 
    interface SomeInterface { 
        void doSomeStuff(); 
    } 
    private static void invokeSomeStuff(SomeInterface someInterface) { 
        someInterface.doSomeStuff(); 
    }   
    public static void main(String[] args) { 
        invokeSomeStuff(()->{ 
                System.out.println("doSomeStuff called"); 
        }); 
    } 
} 

因此,在这里,SomeInterfaceinvokeSomeStuff()的定义保持不变。唯一的不同之处在于传递SomeInterface的实例。我们不是用一个新的SomeInstance创建SomeInstance的实例,而是写了一个表达式(粗体),这个表达式看起来非常像数学函数表达式(除了显然的System.out.println())。这个表达式被称为lambda 表达式

难道这不是很棒吗?你不需要创建接口的实例,然后重写方法等所有这些操作。你所做的是一个非常简单的表达式。这个表达式将被用作接口内部doSomeStuff()方法的主体。

两个程序输出的结果相同;如下截图所示:

Java 没有 lambda 的类型;你只能使用 lambda 在运行时创建类和接口的实例。Java 中 lambda 的唯一好处是它使 Java 程序更容易阅读(对人类来说),并减少了行数。

我们实际上不能责怪 Java。毕竟,Java 基本上是一种纯面向对象的语言。另一方面,Kotlin 是面向对象和函数式编程范式的完美结合;它将这两个世界更紧密地联系在一起。用我们的话说,如果你想在了解面向对象编程的基础上开始函数式编程,Kotlin 是最好的语言。

所以,不再有讲座了,让我们继续看代码。现在让我们看看同样的程序在 Kotlin 中的样子:

fun invokeSomeStuff(doSomeStuff:()->Unit) { 
    doSomeStuff() 
} 
fun main(args: Array<String>) { 
    invokeSomeStuff({ 
        println("doSomeStuff called"); 
    }) 
} 

是的,这就是完整的程序(好吧,除了 import 语句和包名)。我知道你有点困惑;你在问这真的是同一个程序吗?接口定义在哪里呢?嗯,在 Kotlin 中实际上并不需要。

invokeSomeStuff() 函数实际上是一个高阶函数(将在下一节中介绍);我们向那里传递我们的 lambda,它直接调用该函数。

太棒了,不是吗?Kotlin 有很多与 lambda 相关的特性。让我们看看它们。

函数作为属性

Kotlin 还允许我们将函数作为属性。函数作为属性意味着函数可以用作属性。

例如,看看以下示例:

fun main(args: Array<String>) { 
    val sum = { x: Int, y: Int -> x + y }  
    println("Sum ${sum(10,13)}") 
    println("Sum ${sum(50,68)}") 
} 

在前面的程序中,我们创建了一个属性 sum,它实际上将持有一个用于添加传递给它的两个数字的函数。

虽然 sum 是一个 val 属性,但它持有的是一个函数(或 lambda),我们可以像调用常规函数一样调用这个函数;这里没有任何区别。

如果你好奇,以下就是输出:

现在,让我们讨论 lambda 的语法。

在 Kotlin 中,lambda 总是包含在大括号内。这使得 lambda 很容易识别,与 Java 不同,在 Java 中参数/参数位于大括号之外。在 Kotlin 中,参数/参数位于大括号内,由 (->) 与函数的逻辑分开。lambda 中的最后一个语句(可能只是一个变量/属性名或另一个函数调用)被视为返回语句。所以,lambda 最后一个语句的评估结果就是 lambda 的返回值。

此外,如果你的函数是单参数函数,你也可以省略属性名。那么,如果你不指定名称,你如何使用那个参数呢?Kotlin 为你提供了一个默认的 it 属性,用于单参数 lambda,其中你未指定属性名。

因此,让我们修改之前的 lambda 来添加它。看看下面的代码:

reverse = { 
        var n = it 
        var revNumber = 0 
        while (n>0) { 
            val digit = n%10 
            revNumber=revNumber*10+digit 
            n/=10 
        } 
        revNumber 
} 

我们跳过了完整的程序和输出,因为它们保持不变。

你一定注意到了,我们将函数参数的值赋给了另一个 var 属性(无论是当参数被命名还是用 it 表示时)。原因是,在 Kotlin 中,函数参数是不可变的,但与反向数字程序相比,我们需要一种改变值的方法;因此,我们将值赋给一个可变的 var 属性。

现在,你有了 lambda 作为属性,但它们的数据类型是什么呢?每个属性/变量都有一个数据类型(即使类型是推断的),那么 lambda 呢?让我们看看以下示例:

fun main(args: Array<String>) { 
    val reverse:(Int)->Int//(1) 
    reverse = {number -> 
        var n = number 
        var revNumber = 0 
        while (n>0) { 
            val digit = n%10 
            revNumber=revNumber*10+digit 
            n/=10 
        } 
        revNumber 
    }// (2) 
    println("reverse 123 ${reverse(123)}") 
    println("reverse 456 ${reverse(456)}") 
    println("reverse 789 ${reverse(789)}") 
} 

在前面的程序中,我们声明了一个reverse属性作为函数。在 Kotlin 中,当你将属性声明为函数时,你应该在括号内提及参数/参数的数据类型,然后是一个箭头,然后是函数的返回类型;如果函数不打算返回任何内容,你应该提及Unit。在将函数声明为属性时,你不需要指定参数/参数的名称,而在定义/分配函数到属性时,你可以省略提供属性的数据类型。

以下是输出结果:

图片

因此,我们在 Kotlin 中对 lambda 和函数作为属性有了一个很好的理解。现在,让我们继续探讨高阶函数。

高阶函数

高阶函数是一个接受另一个函数作为参数或返回另一个函数的函数。我们刚刚看到我们可以如何将函数用作属性,所以很容易看出我们可以接受另一个函数作为参数,或者我们可以从函数中返回另一个函数。如前所述,技术上接收或返回另一个函数(可能不止一个)或两者兼而有之的函数被称为高阶****函数

在 Kotlin 的第一个 lambda 示例中,invokeSomeStuff函数是一个高阶函数。

以下是一个高阶函数的另一个示例:

fun performOperationOnEven(number:Int,operation:(Int)->Int):Int { 
    if(number%2==0) { 
        return operation(number) 
    } else { 
        return number 
    } 
} 
fun main(args: Array<String>) { 
    println("Called with 4,(it*2): ${performOperationOnEven(4, 
            {it*2})}") 
    println("Called with 5,(it*2): ${performOperationOnEven(5, 
            {it*2})}") 
} 

在前面的程序中,我们创建了一个高阶函数——performOperationOnEven,它将接受一个Int和一个对那个Int执行的操作的 lambda 表达式。唯一的限制是,如果该Int是偶数,该函数才会执行该操作。

这不是足够简单吗?让我们看看以下输出:

图片

在我们之前的所有示例中,我们看到了如何将一个函数(lambda)传递给另一个函数。然而,这并不是高阶函数的唯一特性。高阶函数还允许你从它返回一个函数。

那么,让我们来探索一下。看看以下示例:

fun getAnotherFunction(n:Int):(String)->Unit { 
    return { 
        println("n:$n it:$it") 
    } 
} 
fun main(args: Array<String>) { 
    getAnotherFunction(0)("abc") 
    getAnotherFunction(2)("def") 
    getAnotherFunction(3)("ghi") 
} 

在前面的程序中,我们创建了一个函数getAnotherFunction,它将接受一个Int参数,并返回一个接受一个String值并返回Unit的函数。该return函数打印其参数(一个String)及其父参数(一个Int)。

看看以下输出:

图片

在 Kotlin 中,技术上你可以有嵌套的高阶函数到任何深度。然而,这样做可能会造成更多的伤害而不是帮助,甚至可能破坏可读性。所以,你应该避免它们。

纯函数和副作用

因此,我们已经学习了 lambda 函数和高阶函数。它们是函数式编程中最有趣和最重要的主题之一。在本节中,我们将讨论副作用和纯函数。

因此,让我们首先定义副作用。然后我们将逐步转向纯函数。

副作用

在计算机程序中,当一个函数修改其自身作用域之外的任何对象/数据时,这被称为副作用。例如,我们经常编写修改全局或静态属性的函数,修改其参数之一,抛出异常,将数据写入显示或文件,甚至调用具有副作用的另一个函数。

例如,看看以下程序:

class Calc { 
    var a:Int=0 
    var b:Int=0 
    fun addNumbers(a:Int = this.a,b:Int = this.b):Int {  
        this.a = a 
        this.b = b 
        return a+b 
    } 
} 
fun main(args: Array<String>) { 
    val calc = Calc() 
    println("Result is ${calc.addNumbers(10,15)}") 
} 

前面的程序是一个简单的面向对象程序。然而,它包含副作用。addNumbers() 函数修改了 Calc 类的状态,这在函数式编程中是一种不良做法。

虽然我们无法避免一些函数的副作用,尤其是在我们访问 IO 和/或数据库等情况时,但应尽可能避免副作用。

纯函数

纯函数的定义表明,如果一个函数的返回值完全依赖于其参数/参数,那么这个函数可以被称为纯函数。所以,如果我们声明一个函数为 fun func1(x:Int):Int,那么它的返回值将严格依赖于其参数 x;比如说,如果你用 3 N 次调用 func1,那么每次调用的返回值都将相同。

定义还说明,纯函数不应主动或被动地引起副作用,也就是说,它不应直接引起副作用,也不应调用任何引起副作用的函数。

纯函数可以是 lambda 函数或命名函数。

那么,为什么它们被称为纯函数呢?原因很简单。编程函数起源于数学函数。随着时间的推移,编程函数演变成了包含多个任务并执行与传递参数处理无直接关系的匿名操作。因此,那些仍然类似于数学函数的函数被称为纯函数。

那么,让我们修改我们之前的程序,使其成为一个纯函数:

fun addNumbers(a:Int = 0,b:Int = 0):Int { 
    return a+b 
} 

fun main(args: Array<String>) { 
    println() 
} 

非常简单,不是吗?我们跳过了输出,因为这个程序真的很简单。

概述

在本章中,我们学习了函数,如何使用它们以及它们的分类。我们还介绍了 lambda 函数和高阶函数。我们学习了纯函数和副作用。

下一章将带你深入了解函数。正如我已经提到的,你需要掌握函数才能正确学习函数式编程。那么,你在等什么呢?现在就翻到下一页。

第五章:更多关于函数的内容

在前面的章节中,我们介绍了 Kotlin 函数的许多特性。但现在我们将扩展这些特性,其中大多数是从其他语言借用的,但它们在 Kotlin 的整体目标和风格中有一个新的转折——类型安全和实用简洁。

一些特性,例如领域特定语言DSLs),允许开发者扩展语言以适应在 Kotlin 最初设计时未考虑到的领域。

在本章结束时,你将有一个关于所有函数特性的整体概念,包括:

  • 扩展函数

  • 操作符重载

  • 类型安全的构建器

  • 内联函数

  • 递归和核心递归

单表达式函数

到目前为止,所有我们的例子都是以常规方式声明的。

sum函数接受两个Int类型的值并将它们相加。以常规方式声明,我们必须提供一个带有花括号和显式return的主体:

fun sum(a:Int, b:Int): Int {
   return a + b
}

我们的sum函数其主体在花括号内声明,并包含一个return子句。但如果我们的函数只是一个表达式,它也可以写成一行:

fun sum(a:Int, b:Int): Int = a + b

所以,没有花括号,没有return子句,并且有一个等于(=)符号。如果你注意的话,它看起来与 lambda 非常相似。

如果你想要减少更多字符,你还可以使用类型推断:

fun sum(a:Int, b:Int) = a + b

当返回类型非常明显时,可以使用类型推断来指定函数的返回类型。一个很好的经验法则是对于简单类型,如数值、布尔值、字符串和简单的data class构造函数使用它。任何更复杂的事情,特别是如果函数执行任何转换,都应该有显式的类型。你未来的自己会为此感到高兴!

参数

一个函数可以有零个或多个参数。我们的函数basicFunction接受两个参数,如下面的代码所示:

fun basicFunction(name: String, size: Int) {

}

每个参数都定义为parameterName: ParameterType,在我们的例子中,name: Stringsize: Int。这里没有什么新的。

vararg

当参数有两种我们已经讨论过的类型时,事情变得有趣——vararg和 lambda:

fun aVarargFun(vararg names: String) {
   names.forEach(::println)
}

fun main(args: Array<String>) {
   aVarargFun()
   aVarargFun("Angela", "Brenda", "Caroline")
}

一个带有vararg修饰符的参数的函数可以用零个或多个值来调用:

fun multipleVarargs(vararg names: String, vararg sizes: Int) {
// Compilation error, "Multiple vararg-parameters are prohibited"
}

一个函数不能有多个vararg参数,即使是不同类型的也不行。

Lambda

我们已经讨论了如果函数的最后一个参数是一个 lambda,它不能放在括号外和花括号内,就像 lambda 本身是控制结构的主体一样。

我们在第二章“开始函数式编程”的“一等和高级函数”部分介绍了这个unless函数。让我们看看以下代码:

fun unless(condition: Boolean, block: () -> Unit) {
   if (!condition) block()
}

unless(someBoolean) {
   println("You can't access this website")
}

现在,如果我们结合vararg和 lambda 会发生什么?让我们在下面的代码片段中检查一下:

fun <T, R> transform(vararg ts: T, f: (T) -> R): List<R> = ts.map(f)

Lambda 可以放在带有vararg参数的函数的末尾:

transform(1, 2, 3, 4) { i -> i.toString() }

让我们稍微冒险一点,一个 lambda 的vararg参数:

fun <T> emit(t: T, vararg listeners: (T) -> Unit) = listeners.forEach { listener ->
    listener(t)
}

emit(1){i -> println(i)} //Compilation error. Passing value as a vararg is only allowed inside a parenthesized argument list

我们不能将 lambda 放在括号外,但我们可以将多个 lambda 放在括号内:

emit(1, ::println, {i -> println(i * 2)})

命名参数

理想情况下,我们的函数不应该有太多的参数,但这并不总是如此。一些函数倾向于很大,例如,data class 构造函数(构造函数在技术上是一个返回新实例的函数)。

参数众多的函数有什么问题?

  • 它们很难使用。这可以通过下一节中将要介绍的默认参数来缓解或修复。

  • 它们很难阅读——命名参数来拯救。

  • 它们可能做得太多。你确定你的函数不是太大了吗?尝试重构它并清理。寻找可能的副作用和其他有害做法。一个特殊情况是 data class 构造函数,因为它们只是自动生成的赋值。

使用命名参数,你可以增加任何函数调用的可读性。

让我们以 data class 构造函数为例:

typealias Kg = Double
typealias cm = Int

data class Customer(val firstName: String,
               val middleName: String,
               val lastName: String,
               val passportNumber: String,
               val weight: Kg,
               val height: cm)

正常调用将看起来像这样:

val customer1 = Customer("John", "Carl", "Doe", "XX234", 82.3, 180)

但包括命名参数将增加读者/维护者可获得的信息量,并减少心理工作。我们也可以以更方便或更有意义的顺序传递参数:

val customer2 = Customer(
      lastName = "Doe",
      firstName = "John",
      middleName = "Carl",
      height = 180,
      weight = 82.3,
      passportNumber = "XX234")

当与 vararg 参数结合使用时,命名参数非常有用:

fun paramAfterVararg(courseId: Int, vararg students: String, roomTemperature: Double) {
    //Do something here
}

paramAfterVararg(68, "Abel", "Barbara", "Carl", "Diane", roomTemperature = 18.0)

高阶函数上的命名参数

通常当我们定义高阶函数时,我们从不为 lambda(s) 命名参数:

fun high(f: (Int, String) -> Unit) {
   f(1, "Romeo")
}

high { q, w ->
    //Do something
}

但可以添加它们。因此,f lambda 现在有了命名的参数——agename

fun high(f: (age:Int, name:String) -> Unit) {
   f(1, "Romeo")
}

这不会改变任何行为,只是为了更清晰地说明这个 lambda 的预期用途:

fun high(f: (age:Int, name:String) -> Unit) {
   f(age = 3, name = "Luciana") //compilation error
}

但使用命名参数调用 lambda 是不可能的。在我们的例子中,使用名称调用 f 会导致编译错误。

默认参数

在 Kotlin 中,函数参数可以有默认值。对于 ProgrammerfavouriteLanguageyearsOfExperience 数据类有默认值(记住,构造函数也是一个函数):

data class Programmer(val firstName: String,
                 val lastName: String,
                 val favouriteLanguage: String = "Kotlin",
                 val yearsOfExperience: Int = 0)

因此,Programmer 可以只使用两个参数来创建:

val programmer1 = Programmer("John", "Doe")

但如果你想要传递 yearsOfExperience,它必须作为一个命名参数:

val programmer2 = Programmer("John", "Doe", 12) //Error

val programmer2 = Programmer("John", "Doe", yearsOfExperience = 12) //OK

如果你想要传递所有参数,你仍然可以这样做,但如果你不使用命名参数,它们必须以正确的顺序提供:

val programmer3 = Programmer("John", "Doe", "TypeScript", 1)

扩展函数

毫无疑问,Kotlin 最好的特性之一是扩展函数。扩展函数允许你使用新函数修改现有类型:

fun String.sendToConsole() = println(this)

fun main(args: Array<String>) {
   "Hello world! (from an extension function)".sendToConsole()
}

要向现有类型添加扩展函数,你必须将函数的名称写在类型名称旁边,并用点 (.) 连接。

在我们的例子中,我们向 String 类型添加了一个扩展函数 (sendToConsole())。在函数体内部,this 指的是 String 类型的实例(在这个扩展函数中,string 是接收器类型)。

除了点(.)和this,扩展函数与普通函数具有相同的语法规则和功能。实际上,在幕后,扩展函数是一个普通函数,其第一个参数是接收者类型的值。因此,我们的sendToConsole()扩展函数等同于以下代码:

fun sendToConsole(string: String) = println(string)

sendToConsole("Hello world! (from a normal function)")

因此,实际上我们并没有通过新函数修改类型。扩展函数是一种非常优雅地编写实用函数的方法,易于编写,使用起来非常有趣,阅读起来也很愉快——双赢。这也意味着扩展函数有一个限制——它们不能访问this的私有成员,而一个合适的成员函数可以访问实例内的所有内容:

class Human(private val name: String)

fun Human.speak(): String = "${this.name} makes a noise" //Cannot access 'name': it is private in 'Human'

调用扩展函数与调用普通函数相同——使用接收者类型的实例(在扩展函数内部将引用为this),通过名称调用函数。

扩展函数与继承

当我们谈论继承时,成员函数和扩展函数之间有很大的区别。

开放类Canine有一个子类Dog。一个独立的函数printSpeak接收一个类型为Canine的参数,并打印函数speak(): String的结果内容:

open class Canine {
   open fun speak() = "<generic canine noise>"
}

class Dog : Canine() {
   override fun speak() = "woof!!"
}

fun printSpeak(canine: Canine) {
   println(canine.speak())
}

我们已经在第一章,“Kotlin – 数据类型、对象和类”,在继承部分中讨论了这一点。具有open方法的开放类(成员函数)可以被扩展并改变其行为。调用speak函数的行为将根据实例的类型而有所不同。

printSpeak函数可以用任何is-a Canine类的实例调用,无论是Canine本身还是任何子类:

printSpeak(Canine())
printSpeak(Dog())

如果我们执行此代码,我们可以在控制台上看到以下内容:

虽然两者都是Canine,但在两种情况下speak的行为都不同,因为子类覆盖了父类实现。

但与扩展函数不同,许多事情都不同。

与前面的示例一样,Feline是一个由Cat类扩展的开放类。但现在speak是一个扩展函数:

open class Feline

fun Feline.speak() = "<generic feline noise>"

class Cat : Feline()

fun Cat.speak() = "meow!!"

fun printSpeak(feline: Feline) {
   println(feline.speak())
}

扩展函数不需要标记为override,因为我们没有覆盖任何内容:

printSpeak(Feline())
printSpeak(Cat()

如果我们执行此代码,我们可以在控制台上看到以下内容:

在这种情况下,两次调用都产生相同的结果。虽然一开始看起来很混乱,但一旦分析清楚,就会变得清晰。我们调用了两次Feline.speak()函数;这是因为我们传递给printSpeak(Feline)函数的每个参数都是一个Feline

open class Primate(val name: String)

fun Primate.speak() = "$name: <generic primate noise>"

open class GiantApe(name: String) : Primate(name)

fun GiantApe.speak() = "${this.name} :<scary 100db roar>"

fun printSpeak(primate: Primate) {
 println(primate.speak())
}

printSpeak(Primate("Koko"))
printSpeak(GiantApe("Kong"))

如果我们执行此代码,我们可以在控制台上看到以下内容:

在这种情况下,行为与前面的示例相同,但使用了正确的name值。说到这里,我们可以用namethis.name来引用name;两者都是有效的。

扩展函数作为成员

扩展函数可以声明为类的成员。声明了扩展函数的类的实例称为调度接收器

Caregiver公开类内部定义了针对两个不同类FelinePrimate的扩展函数:

open class Caregiver(val name: String) {
   open fun Feline.react() = "PURRR!!!"

   fun Primate.react() = "*$name plays with ${this@Caregiver.name}*"

   fun takeCare(feline: Feline) {
      println("Feline reacts: ${feline.react()}")
   }

   fun takeCare(primate: Primate){
      println("Primate reacts: ${primate.react()}")
   }
}

这两个扩展函数都打算在Caregiver的实例内部使用。实际上,如果它们不是公开的,将成员扩展函数标记为私有是一个好习惯。

Primate.react()的情况下,我们使用了PrimateCaregiver中的name值。要访问具有名称冲突的成员,扩展接收器(this)具有优先级,要访问调度接收器的成员,必须使用限定this语法。调度接收器的其他没有名称冲突的成员可以使用而不需要限定this

不要被我们已经覆盖的各种this的用法所迷惑:

  • 在类内部,this意味着该类的实例

  • 在扩展函数内部,this意味着接收器类型的实例,就像我们工具函数中第一个参数的优雅语法一样:

class Dispatcher {
   val dispatcher: Dispatcher = this

   fun Int.extension(){
      val receiver: Int = this
      val dispatcher: Dispatcher = this@Dispatcher
   }
}

回到我们的动物园示例,我们实例化了一个Caregiver,一个Cat和一个Primate,并且我们使用这两个动物实例调用了Caregiver.takeCare函数:

val adam = Caregiver("Adam")

val fulgencio = Cat()

val koko = Primate("Koko")

adam.takeCare(fulgencio)
adam.takeCare(koko)

如果我们执行此代码,我们可以在控制台上看到以下内容:

任何动物园都需要兽医。类Vet扩展了Caregiver

open class Vet(name: String): Caregiver(name) {
   override fun Feline.react() = "*runs away from $name*"
}

我们使用不同的实现覆盖了Feline.react()函数。我们还直接使用了Vet类的名称,因为Feline类没有属性名:

val brenda = Vet("Brenda")

listOf(adam, brenda).forEach { caregiver ->
   println("${caregiver.javaClass.simpleName} ${caregiver.name}")
   caregiver.takeCare(fulgencio)
   caregiver.takeCare(koko)
}

之后,我们得到以下输出:

具有冲突名称的扩展函数

当扩展函数与成员函数具有相同的名称时会发生什么?

Worker类有一个work(): String函数和一个私有函数rest(): String。我们还有两个具有相同签名的扩展函数,workrest

class Worker {
   fun work() = "*working hard*"

   private fun rest() = "*resting*"
}

fun Worker.work() = "*not working so hard*"

fun <T> Worker.work(t:T) = "*working on $t*"

fun Worker.rest() = "*playing video games*"

具有相同签名的扩展函数不会导致编译错误,但会发出警告:“扩展函数被成员覆盖:public final fun work(): String”

声明一个与成员函数具有相同签名的函数是合法的,但成员函数始终具有优先级,因此扩展函数永远不会被调用。当成员函数是私有的时,这种行为会改变,在这种情况下,扩展函数具有优先级。

使用扩展函数也可以重载现有的成员函数:

val worker = Worker()

println(worker.work())

println(worker.work("refactoring"))

println(worker.rest())

在执行时,work()调用成员函数,而work(String)rest()是扩展函数:

对象的扩展函数

在 Kotlin 中,对象是一种类型,因此它们可以有函数,包括扩展函数(以及其他一些功能,如扩展接口等)。

我们可以向对象 Builder 添加一个 buildBridge 扩展函数:

object Builder {

}

fun Builder.buildBridge() = "A shinny new bridge"

我们可以包含伴随对象。类 Designer 有两个内部对象,companion 对象和 Desk 对象:

class Designer {
   companion object {

   }

   object Desk {

   }
}

fun Designer.Companion.fastPrototype() = "Prototype"

fun Designer.Desk.portofolio() = listOf("Project1", "Project2")

调用此函数的工作方式与任何正常对象成员函数一样:

Designer.fastPrototype()
Designer.Desk.portofolio().forEach(::println)

中缀函数

只有一个参数的函数(普通或扩展)可以标记为 中缀 并使用 中缀 表示法。对于某些领域,例如数学和代数运算,中缀 表示法有助于自然地表达代码。

让我们在 Int 类型上添加一个 中缀 扩展函数,superOperation(这只是一个带有花哨名称的常规求和):

infix fun Int.superOperation(i: Int) = this + i

fun main(args: Array<String>) {
   1 superOperation 2
   1.superOperation(2)
}

我们可以使用 superOperation 函数以及 中缀 表示法或常规表示法。

另一个 中缀 表示法常用领域是断言库,例如 HamKrest (github.com/npryce/hamkrest) 或 Kluent (github.com/MarkusAmshove/Kluent)。用自然、易于理解的语言编写规范代码是一个巨大的优势。

Kluent 断言看起来像自然的英语表达:

"Kotlin" shouldStartWith "Ko"

Kluent 还提供了一个反引号版本,以增强可读性:

"Kotlin" `should start with` "Ko"

反引号(`)允许你编写任意标识符,包括 Kotlin 中保留的单词。现在,你可以编写自己的表情包函数:

你可以将许多 中缀 函数链式调用以生成内部 DSL,或者重新创建经典梗:

object All {
   infix fun your(base: Pair<Base, Us>) {}
}

object Base {
   infix fun are(belong: Belong) = this
}

object Belong

object Us

fun main(args: Array<String>) {
   All your (Base are Belong to Us)
}

your 函数接收 Pair<Base, Us> 作为参数(这是一种元组,它随 Kotlin 标准库提供并广泛使用)和 中缀 扩展函数 <K, V> K.to(v: V) 使用接收者作为第一个成员,参数作为第二个参数(to 可以用任何类型的组合调用)。

操作符重载

操作符重载 是一种多态形式。一些操作符在不同类型上会改变行为。经典的例子是操作符加 (+)。在数值上,加是求和操作,在 String 上是连接。操作符重载是提供自然表面 API 的有用工具。假设我们正在编写 TimeDate 库;在时间单位上定义加法和减法操作符将是自然的。

Kotlin 允许你使用函数定义自己的或现有类型的操作行为,无论是普通函数还是扩展函数,只要使用 operator 修饰符标记即可:

class Wolf(val name:String) {
   operator fun plus(wolf: Wolf) = Pack(mapOf(name to this, wolf.name to wolf))
}

class Pack(val members:Map<String, Wolf>)

fun main(args: Array<String>) {
   val talbot = Wolf("Talbot")
   val northPack: Pack = talbot + Wolf("Big Bertha") // talbot.plus(Wolf("..."))
}

操作符函数加返回一个 Pack 值。要调用它,你可以使用 中缀 操作符方式(Wolf + Wolf)或常规方式(Wolf.plus(Wolf))。

关于 Kotlin 中操作符重载需要注意的一点是——你可以在 Kotlin 中重载的操作符是有限的;你不能创建任意的操作符。

二元操作符

二元操作符接收一个参数(有一些例外——invoke 和索引访问)。

Pack.plus 扩展函数接收一个 Wolf 参数并返回一个新的 Pack。注意,MutableMap 也具有加号(+)操作符:

operator fun Pack.plus(wolf: Wolf) = Pack(this.members.toMutableMap() + (wolf.name to wolf))

val biggerPack = northPack + Wolf("Bad Wolf")

下表将展示所有可能的可重载的二进制操作符:

操作符 等效 说明
x + y x.plus(y)
x - y x.minus(y)
x * y x.times(y)
x / y x.div(y)
x % y x.rem(y) 从 Kotlin 1.1 版本开始,之前为 mod.
x..y x.rangeTo(y)
x in y y.contains(x)
x !in y !y.contains(x)
x += y x.plussAssign(y) 必须返回 Unit.
x -= y x.minusAssign(y) 必须返回 Unit.
x *= y x.timesAssign(y) 必须返回 Unit.
x /= y x.divAssign(y) 必须返回 Unit.
x %= y x.remAssign(y) 从 Kotlin 1.1 版本开始,之前为 modAssign。必须返回 Unit.
x == y x?.equals(y) ?: (y === null) 检查 null.
x != y !(x?.equals(y) ?: (y === null)) 检查 null.
x < y x.compareTo(y) < 0 必须返回 Int.
x > y x.compareTo(y) > 0 必须返回 Int.
x <= y x.compareTo(y) <= 0 必须返回 Int.
x >= y x.compareTo(y) >= 0 必须返回 Int.

调用

回到 第二章,开始函数式编程,在 一等和高级函数 部分,当我们介绍 lambda 函数时,展示了 Function1 的定义:

/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}

invoke 函数是一个操作符,一个有趣的操作符。invoke 操作符可以在没有 name 的情况下被调用。

Wolf 类有一个 invoke 操作符:

enum class WolfActions {
   SLEEP, WALK, BITE
}

class Wolf(val name:String) {
   operator fun invoke(action: WolfActions) = when (action) {
      WolfActions.SLEEP -> "$name is sleeping"
      WolfActions.WALK -> "$name is walking"
      WolfActions.BITE -> "$name is biting"
   }
}

fun main(args: Array<String>) {
   val talbot = Wolf("Talbot")

   talbot(WolfActions.SLEEP) // talbot.invoke(WolfActions.SLEEP)
}

正因如此,我们可以直接用括号调用 lambda 函数;实际上,我们是在调用 invoke 操作符。

下表将展示 invoke 函数的不同声明及其不同参数数量:

操作符 等效 说明
x() x.invoke()
x(y) x.invoke(y)
x(y1, y2) x.invoke(y1, y2)
x(y1, y2..., yN) x.invoke(y1, y2..., yN)

索引访问

索引访问操作符是使用方括号([])的数组读写操作,用于具有类似 C 语法语法的语言。在 Kotlin 中,我们使用 get 操作符进行读取,使用 set 进行写入。

使用 Pack.get 操作符,我们可以将 Pack 当作数组使用:

operator fun Pack.get(name: String) = members[name]!!

val badWolf = biggerPack["Bad Wolf"]

大多数 Kotlin 数据结构都有一个 get 操作符的定义,在这种情况下,Map<K, V> 返回一个 V?

下表将展示 get 函数的不同声明及其不同参数数量:

操作符 等效 说明
x[y] x.get(y)
x[y1, y2...] x.get(y1, y2...)
x[y1, y2...] x.get(y1, y2...)

set 操作符具有类似的语法:

enum class WolfRelationships {
   FRIEND, SIBLING, ENEMY, PARTNER
}

operator fun Wolf.set(relationship: WolfRelationships, wolf: Wolf) {
   println("${wolf.name} is my new $relationship")
}

talbot[WolfRelationships.ENEMY] = badWolf

getset 运算符可以包含任意代码,但有一个非常著名且古老的约定,即索引访问用于读写。当你编写这些运算符(顺便说一下,所有其他运算符也是如此)时,使用“最小惊讶”原则。将运算符限制在其特定领域的自然含义上,从长远来看,使它们更容易使用和阅读。

下表将展示不同数量参数的 set 的不同声明:

运算符 等效 说明
x[y] = z x.set(y, z) 返回值被忽略
x[y1, y2] = z x.set(y1, y2, z) 返回值被忽略
x[y1, y2..., yN] = z x.set(y1, y2..., yN, z) 返回值被忽略

一元运算符

一元运算符没有参数,并直接作用于分发器。

我们可以向 Wolf 类添加一个 not 运算符:

operator fun Wolf.not() = "$name is angry!!!"

!talbot // talbot.not()

下表将展示所有可能被重载的一元运算符:

运算符 等效 说明
+x x.unaryPlus()
-x x.unaryMinus()
!x x.not()
x++ x.inc() 后缀,它必须是对 var 的调用,应该返回与分发器类型兼容的类型,不应该修改分发器。
x-- x.dec() 后缀,它必须是对 var 的调用,应该返回与分发器类型兼容的类型,不应该修改分发器。
++x x.inc() 前缀,它必须是对 var 的调用,应该返回与分发器类型兼容的类型,不应该修改分发器。
--x x.dec() 前缀,它必须是对 var 的调用,应该返回与分发器类型兼容的类型,不应该修改分发器。

后缀(递增和递减)返回原始值,然后更改变量为运算符返回的值。前缀返回运算符的返回值,然后更改变量为该值。

类型安全的构建器

在前两个部分(中缀函数和运算符重载)之后,我们为构建出色的 DSL 打下了良好的基础。DSL 是一种针对特定领域专门化的语言,与 通用语言GPL)相对。经典的 DSL 示例(即使人们没有意识到)是 HTML(标记)和 SQL(关系数据库查询)。

Kotlin 提供了许多功能来创建内部 DSL(在宿主 GPL 内部运行的 DSL),但我们仍需要介绍一个特性,即类型安全的构建器。类型安全的构建器允许我们以(半)声明性的方式定义数据,并且对于定义 GUI、HTML 标记、XML 等非常有用。

一个漂亮的 Kotlin DSL 示例是 TornadoFX。TornadoFX (tornadofx.io/) 是用于创建 JavaFX 应用程序的 DSL。

我们编写一个 FxApp 类,它扩展了 tornadofx.App 并接收一个 tornadofx.View 类(一个类引用,而不是一个实例):

import javafx.application.Application
import tornadofx.*

fun main(args: Array<String>) {
   Application.launch(FxApp::class.java, *args)
}

class FxApp: App(FxView::class)

class FxView: View() {
   override val root = vbox {
      label("Functional Kotlin")
      button("Press me")
   }
}

在不到 20 行代码中,包括导入和主函数,我们可以创建一个 GUI 应用程序:

图片

当然,现在它什么也不做,但用 TornadoFX 创建一个 JavaFX 应用程序很简单,如果你与 Java 进行比较。有 JavaFX 经验的人可能会说,你可以用 FXML(一种用于构建 JavaFX 布局的声明性 XML 语言)达到类似的效果,但就像任何其他 XML 文件一样,编写和维护都很困难,而 TornadoFX 的 DSL 更简单、更灵活,并且使用 Kotlin 的类型安全性进行编译。

但类型安全的构建器是如何工作的呢?

让我们从 Kotlin 标准库的一个例子开始:

val joinWithPipe = with(listOf("One", "Two", "Three")){
   joinToString(separator = "|")
}

我们可以在其他语言中找到 with 块,例如 JavaScript 和 Visual Basic(包括 .Net)。with 块是一种语言结构,它允许我们使用传递为参数的值上的任何属性或方法。但在 Kotlin 中,with 不是一个保留关键字,而是一个具有特殊参数类型的普通函数。

让我们看看 with 声明:

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}

第一个参数是类型 T 的任何值,一个接收器(就像扩展函数一样?),第二个参数 block 是类型 T.() -> R 的函数。在 Kotlin 的文档中,这种函数被称为具有接收器的 函数类型,并且对于 T 的任何实例,我们都可以调用 block 函数。不用担心 inline 修饰符,我们将在下一节中介绍它。

理解具有接收器的函数类型的一个技巧是将它想象成一个扩展函数。看看那个熟悉的点(.)声明,并在函数内部,我们可以使用 this 来使用接收器类型的任何成员,就像扩展函数一样。

再举一个例子怎么样?让我们看看它:

val html = buildString {
   append("<html>\n")
   append("\t<body>\n")
   append("\t\t<ul>\n")
   listOf(1, 2, 3).forEach { i ->
      append("\t\t\t<li>$i</li>\n")
   }
   append("\t\t<ul>\n")
   append("\t</body>\n")
   append("</htm

l>")
}

buildString 函数接收一个 StringBuilder.() -> Unit 参数并返回一个 String;其声明非常简单:

public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
        StringBuilder().apply(builderAction).toString()

apply 函数是一个类似于 with 的扩展函数,但它返回的是接收器实例,而不是 R。通常,apply 用于 初始化实例

public inline fun <T> T.apply(block: T.() -> Unit): T {    
    block()
    return this
}

如您所见,所有这些函数都非常容易理解,但它们极大地增加了 Kotlin 的实用性和可读性。

创建 DSL

我的一大爱好是骑自行车。运动的情感、努力、健康益处以及欣赏风景都是一些好处(我可以继续说下去)。

我想创建一种方式来记录我的自行车及其组件。在原型阶段,我会使用 XML,但以后我们可以改为不同的实现:

<bicycle description="Fast carbon commuter">
    <bar material="ALUMINIUM" type="FLAT">
    </bar>
    <frame material="CARBON">
        <wheel brake="DISK" material="ALUMINIUM">
        </wheel>
    </frame>
    <fork material="CARBON">
        <wheel brake="DISK" material="ALUMINIUM">
        </wheel>
    </fork>
</bicycle>  

这是在 Kotlin 中创建类型安全构建器的完美场景。

最后,我的 bicycle DSL 应该看起来像这样:

fun main(args: Array<String>) {
   val commuter = bicycle {
      description("Fast carbon commuter")
      bar {
         barType = FLAT
         material = ALUMINIUM
      }
      frame {
         material = CARBON
         backWheel {
            material = ALUMINIUM
            brake = DISK
         }
      }
      fork {
         material = CARBON
         frontWheel {
            material = ALUMINIUM
            brake = DISK
         }
      }
   }

   println(commuter)
}

我的 DSL 是常规的 Kotlin 代码,编译速度快,我的 IDE 会帮我自动完成代码,并在我出错时提醒我——这是一个双赢的局面。

让我们从程序开始:

interface Element {
   fun render(builder: StringBuilder, indent: String)
}

在我的 DSL 中的 bicycle 的所有部分都将扩展/实现 Element 接口:

@DslMarker
annotation class ElementMarker

@ElementMarker
abstract class Part(private val name: String) : Element {
   private val children = arrayListOf<Element>()
   protected val attributes = hashMapOf<String, String>()

   protected fun <T : Element> initElement(element: T, init: T.() -> Unit): T {
      element.init()
      children.add(element)
      return element
   }

   override fun render(builder: StringBuilder, indent: String) {
      builder.append("$indent<$name${renderAttributes()}>\n")
      children.forEach { c -> c.render(builder, indent + "\t") }
      builder.append("$indent</$name>\n")
   }

   private fun renderAttributes(): String = buildString {
      attributes.forEach { attr, value -> append(" $attr=\"$value\"") }
   }

   override fun toString(): String = buildString {
      render(this, "")
   }
}

Part 是所有我的部分的基类;它有 childrenattributes 属性;它还继承了具有 XML 实现的 Element 接口。改为不同的格式(JSON、YAML 等)不应太难。

initElement 函数接收两个参数,一个元素 T 和一个接收器为 T.() -> Unitinit 函数。内部,init 函数被执行,并将元素添加为子元素。

Part 使用 @ElementMarker 注解,该注解本身使用 @DslMarker 注解。它防止内部元素到达外部元素。

在这个例子中,我们可以使用 frame

val commuter = bicycle {
   description("Fast carbon commuter")
   bar {
      barType = FLAT
      material = ALUMINIUM
      frame {  } //compilation error
   }

仍然可以使用 this 来显式执行:

val commuter = bicycle {
   description("Fast carbon commuter")
   bar {
      barType = FLAT
      material = ALUMINIUM
      this@bicycle.frame{ }
   }

现在,几个枚举来描述材料、杆类型和刹车:

enum class Material {
   CARBON, STEEL, TITANIUM, ALUMINIUM
}

enum class BarType {
   DROP, FLAT, TT, BULLHORN
}

enum class Brake {
   RIM, DISK
}

其中一些部分有 material 属性:

abstract class PartWithMaterial(name: String) : Part(name) {
   var material: Material
      get() = Material.valueOf(attributes["material"]!!)
      set(value) {
         attributes["material"] = value.name
      }
}

我们使用 Material 枚举的 material 属性,并将其存储在 attributes 映射中,转换值来来回回:

class Bicycle : Part("bicycle") {

   fun description(description: String) {
      attributes["description"] = description
   }

   fun frame(init: Frame.() -> Unit) = initElement(Frame(), init)

   fun fork(init: Fork.() -> Unit) = initElement(Fork(), init)

   fun bar(init: Bar.() -> Unit) = initElement(Bar(), init)
}

Bicycle 定义了一个 description 函数和 frameforkbar 的函数。每个函数接收一个 init 函数,我们直接将其传递给 initElement

Frame 有一个后轮的函数:

class Frame : PartWithMaterial("frame") {
   fun backWheel(init: Wheel.() -> Unit) = initElement(Wheel(), init)
}

Wheel 有一个使用 Brake 枚举的 brake 属性:

class Wheel : PartWithMaterial("wheel") {
   var brake: Brake
      get() = Brake.valueOf(attributes["brake"]!!)
      set(value) {
         attributes["brake"] = value.name
      }
}

Bar 有一个用于其类型的属性,使用 BarType 枚举:

class Bar : PartWithMaterial("bar") {

   var barType: BarType
      get() = BarType.valueOf(attributes["type"]!!)
      set(value) {
         attributes["type"] = value.name
      }
}

Fork 定义了一个前轮的函数:

class Fork : PartWithMaterial("fork") {
   fun frontWheel(init: Wheel.() -> Unit) = initElement(Wheel(), init)
}

我们接近完成,我们现在需要的只是我们的 DSL 的入口函数:

fun bicycle(init: Bicycle.() -> Unit): Bicycle {
   val cycle = Bicycle()
   cycle.init()
   return cycle
}

就这样。在 Kotlin 中使用 infix 函数、操作符重载和类型安全的构建器,DSL 非常强大,Kotlin 社区每天都在创建新的和令人兴奋的库。

内联函数

高阶函数非常有用且复杂,但它们伴随着一个缺点——性能惩罚。记住,从 第二章,“开始使用函数式编程”,在“一等和高级函数”部分,lambda 在编译时被转换为一个分配的对象,我们调用它的 invoke 操作符;这些操作消耗 CPU 力和内存,无论它们有多小。

这样的函数:

fun <T> time(body: () -> T): Pair<T, Long> {
   val startTime = System.nanoTime()
   val v = body()
   val endTime = System.nanoTime()
   return v to endTime - startTime
}

fun main(args: Array<String>) {
   val (_,time) = time { Thread.sleep(1000) }
   println("time = $time")
}

一旦编译,它将看起来像这样:

val (_, time) = time(object : Function0<Unit> {
   override fun invoke() {
      Thread.sleep(1000)
   }
})

如果性能对你来说很重要(关键任务应用、游戏、视频流),你可以将高阶函数标记为 inline

inline fun <T> inTime(body: () -> T): Pair<T, Long> {
   val startTime = System.nanoTime()
   val v = body()
   val endTime = System.nanoTime()
   return v to endTime - startTime
}

fun main(args: Array<String>) {
   val (_, inTime) = inTime { Thread.sleep(1000) }
   println("inTime = $inTime")
}

一旦编译,它将看起来像这样:

val startTime = System.nanoTime()
val v = Thread.sleep(1000)
val endTime = System.nanoTime()
val (_, inTime) = (v to endTime - startTime)

整个函数执行被高阶函数的体和 lambda 的体所替换。inline 函数更快,尽管会生成更多的字节码:

每次执行 2.3 毫秒看起来并不多,但长期来看,并且随着更多的优化,可以产生明显的复合效应。

内联限制

内联 lambda 函数有一个重要的限制——它们不能以任何方式被操作(存储、复制等)。

UserService 存储了一个监听器列表 (User) -> Unit

data class User(val name: String)

class UserService {
   val listeners = mutableListOf<(User) -> Unit>()
   val users = mutableListOf<User>() 

   fun addListener(listener: (User) -> Unit) {
      listeners += listener
   }
}

addListener改为内联函数将产生编译错误:

inline fun addListener(listener: (User) -> Unit) {
   listeners += listener //compilation error: Illegal use of inline-parameter listener
}

如果你仔细想想,这是有道理的。当我们内联一个 lambda 表达式时,我们实际上是在替换它的主体,而这并不是我们可以在Map上存储的东西。

我们可以使用noinline修饰符来解决这个问题:

//Warning: Expected performance impact of inlining addListener can be insignificant
inline fun addListener(noinline listener: (User) -> Unit) { 
   listeners += listener
}

在内联函数上使用noinline只会内联高阶函数的主体,但不会内联noinline lambda 参数(内联高阶函数可以同时有:inlinenoinline lambda)。生成的字节码不如完全内联的函数快,编译器会显示警告。

内联 lambda 函数不能用于另一个执行上下文中(局部对象,嵌套 lambda)。

在这个例子中,我们无法在buildUser lambda 中使用transform

inline fun transformName(transform: (name: String) -> String): List<User> {

   val buildUser = { name: String ->
      User(transform(name)) //compilation error: Can't inline transform here
   }

   return users.map { user -> buildUser(user.name) }
}

为了解决这个问题,我们需要一个crossinline修饰符(或者我们可以使用noinline,但会损失相关的性能):

inline fun transformName(crossinline transform: (name: String) -> String): List<User> {

   val buildUser = { name: String ->
      User(transform(name)) 
   }

   return users.map { user -> buildUser(user.name) }
}

fun main(args: Array<String>) {
   val service = UserService()

   service.transformName(String::toLowerCase)
}

生成的代码相当复杂。生成了许多部分:

  • 一个扩展(String) -> User的类来表示buildUser,并在内部使用String::toLowerCase来转换名称

  • 一个普通的内联代码,用于执行使用buildUser类的实例来执行List<User>.map()

  • List<T>.map()是内联的,所以相应的代码也会被生成

一旦你意识到它的限制,内联高阶函数是提高你代码执行速度的绝佳方式。确实,Kotlin 标准库中的许多高阶函数都是inline的。

递归和核心递归

在第二章,“函数式编程入门”,在“递归”部分,我们广泛地介绍了递归(尽管本书的范围不包括所有递归主题)。

我们使用递归编写了经典算法,如斐波那契(我们正在重用第二章,“函数式编程入门”中的tailrecFib):

fun tailrecFib(n: Long): Long {
   tailrec fun go(n: Long, prev: Long, cur: Long): Long {
      return if (n == 0L) {
         prev
      } else {
         go(n - 1, cur, prev + cur)
      }
   }

   return go(n, 0, 1)
}

以及阶乘(同样,这里重用第二章,“函数式编程入门”中的tailrecFactorial):

fun tailrecFactorial(n: Long): Long {
   tailrec fun go(n: Long, acc: Long): Long {
      return if (n <= 0) {
         acc
      } else {
         go(n - 1, n * acc)
      }
   }

   return go(n, 1)
}

在这两种情况下,我们从一个数字开始,并减少它以达到基本条件。

我们还考虑了另一个例子,即FunList

sealed class FunList<out T> {
   object Nil : FunList<Nothing>()

   data class Cons<out T>(val head: T, val tail: FunList<T>) : FunList<T>()

   fun forEach(f: (T) -> Unit) {
      tailrec fun go(list: FunList<T>, f: (T) -> Unit) {
         when (list) {
            is Cons -> {
               f(list.head)
               go(list.tail, f)
            }
            is Nil -> Unit//Do nothing
         }
      }

      go(this, f)
   }

   fun <R> fold(init: R, f: (R, T) -> R): R {
      tailrec fun go(list: FunList<T>, init: R, f: (R, T) -> R): R = when (list) {
         is Cons -> go(list.tail, f(init, list.head), f)
         is Nil -> init
      }

      return go(this, init, f)
   }

   fun reverse(): FunList<T> = fold(Nil as FunList<T>) { acc, i -> Cons(i, acc) }

   fun <R> foldRight(init: R, f: (R, T) -> R): R = this.reverse().fold(init, f)

   fun <R> map(f:(T) -> R): FunList<R> = foldRight(Nil as FunList<R>){ tail, head -> Cons(f(head), tail) }

}

函数forEachfold是递归的。从完整的列表开始,我们减少它直到达到末尾(用Nil表示),这是基本条件。其他函数——reversefoldRightmap只是使用不同变体的fold

因此,一方面,递归将一个复杂值减少到所需的答案,另一方面,核心递归从一个值开始,在此基础上构建以产生复合值(包括可能的无穷数据结构,如Sequence<T>)。

由于我们使用fold函数进行递归操作,我们可以使用unfold函数:

fun <T, S> unfold(s: S, f: (S) -> Pair<T, S>?): Sequence<T> {
   val result = f(s)
   return if (result != null) {
      sequenceOf(result.first) + unfold(result.second, f)
   } else {
      sequenceOf()
   }
}

unfold 函数接受两个参数,一个初始的 S 值,它表示起始或基本步长,以及一个 f lambda,该 lambda 接受该 S 步长并生成一个 Pair<T, S>?(一个可空的 Pair),其中包含要添加到序列中的 T 值和下一个 S 步长。

如果 f(s) 的结果是 null,我们返回一个空序列,否则我们创建一个单值序列并添加 unfold 的新步长的结果。

使用 unfold,我们可以创建一个函数,多次重复单个元素:

fun <T> elements(element: T, numOfValues: Int): Sequence<T> {
   return unfold(1) { i ->
      if (numOfValues > i)
         element to i + 1
      else
         null
   }
}

fun main(args: Array<String>) {
   val strings = elements("Kotlin", 5)
   strings.forEach(::println)
}

elements 函数接受一个元素并重复任意数量的值。内部,它使用 unfold,传递 1 作为初始步长和一个 lambda,该 lambda 接受当前步长并与 numOfValues 进行比较,返回包含相同元素和当前步长 + 1nullPair<T, Int>

这是可以的,但并不非常有趣。那么返回一个阶乘序列怎么样?我们为你准备好了:

fun factorial(size: Int): Sequence<Long> {
   return sequenceOf(1L) + unfold(1L to 1) { (acc, n) ->
      if (size > n) {
         val x = n * acc
         (x) to (x to n + 1)
      } else
         null
   }
}

同样的原理,唯一的区别是,我们的初始步长是 Pair<Long, Int>(第一个元素用于携带计算,第二个用于与大小进行比较)因此,我们的 lambda 应该返回 Pair<Long, Pair<Long, Int>>

斐波那契序列看起来类似:

fun fib(size: Int): Sequence<Long> {
   return sequenceOf(1L) + unfold(Triple(0L, 1L, 1)) { (cur, next</span>, n) ->
      if (size > n) {
         val x = cur + next
         (x) to Triple(next, x, n + 1)
      }
      else
         null
   }
}

除了这个情况,我们使用 Triple<Long, Long, Int>

生成阶乘和斐波那契序列的核心递归实现是计算阶乘或斐波那契数的递归实现的镜像——有些人可能会认为这更容易理解。

概述

通过本章,我们已经涵盖了 Kotlin 函数式编程的大部分特性。我们回顾了如何使用单表达式函数编写更短的函数,不同类型的参数,如何使用扩展函数扩展我们的类型,以及如何使用 infix 函数和运算符编写自然易读的代码。我们还涵盖了使用类型安全构建器的 DSL 基础知识以及如何编写高效的高阶函数。最后,但同样重要的是,我们学习了递归和核心递归。

在下一章中,我们将学习 Kotlin 代理。

第六章:Kotlin 中的委托

在前两章中,我们学习了函数和函数类型在函数式编程中的应用。我们还学习了 Kotlin 提供的各种函数类型。

本章基于 Kotlin 中的委托。委托是 Kotlin 在函数式编程方面的优秀特性。如果你来自非 FP 背景,如 Java,你可能第一次听说委托。因此,在本章中,我们将尽力为你解开这些谜团。

我们将首先学习委托的基础知识,然后逐步过渡到 Kotlin 中委托的实现。

以下列表包含本章将涉及的主题:

  • 委托简介

  • Kotlin 中的委托

  • 委托属性

  • 标准委托

  • 自定义委托

  • 委托映射

  • 本地委托

  • 类委托

那么,让我们开始学习委托。

委托简介

编程中委托的起源来自对象组合。对象组合是将简单对象组合成复杂对象的一种方式。对象组合是许多基本数据结构的关键构建块,包括标签联合、链表和二叉树。

为了使对象组合更可重用(尽可能像继承一样可重用),引入了一种新的模式——委托模式

这种模式允许一个对象拥有一个辅助对象,而这个辅助对象被称为委托。这种模式允许原始对象通过委托给委托辅助对象来处理请求。

虽然委托是一种面向对象的设计模式,但并非所有语言都隐式支持委托(例如 Java,它不隐式支持委托)。在这些情况下,您仍然可以通过显式地将原始对象传递给委托方法作为参数/参数来使用委托

但是,有了语言支持(例如在 Kotlin 中),委托变得更容易,通常感觉就像使用原始变量本身一样。

理解委托

随着时间的推移,委托模式已被证明是继承的更好替代方案。继承是代码重用的强大工具,尤其是在Liskov 替换模型中。此外,面向对象语言对它的直接支持使其更加强大。

然而,继承仍然有一些局限性,例如,一个类在程序执行期间不能动态地更改其超类;此外,如果你对超类进行小的修改,它将直接传播到子类,而这并不是我们每次都想要的。

另一方面,委托是灵活的。你可以把委托看作是多个对象的组合,其中一个对象将其方法调用传递给另一个对象,并称之为委托。如我之前提到的,委托是灵活的;你可以在运行时更改委托。

例如,考虑 Electronics 类和 Refrigerator 类。使用继承时,Refrigerator 应该实现/覆盖 Electronics 的方法调用和属性。然而,使用委托时,Refrigerator 对象将保持对 Electronics 对象的引用,并将方法调用与其一起传递。

既然我们知道 Kotlin 提供了对委托的支持,那么让我们开始学习 Kotlin 中的委托。

Kotlin 中的委托

Kotlin 提供了开箱即用的委托支持。Kotlin 为大多数常见的编程需求提供了某些标准委托。大多数时候,你会发现自己在使用这些标准委托,而不是创建自己的;然而,Kotlin 也允许你根据需求创建自己的委托。

Kotlin 不仅允许对属性进行委托,还允许有委托类。

因此,基本上,Kotlin 中有两种类型的委托,如下所示:

  • 属性委托

  • 类委托

因此,我们先来看一下属性委托,然后我们将继续讨论类委托。

属性委托(标准委托)

在上一节中,我们讨论委托时,我们了解到委托是一种方法传递/转发的技术。

对于属性委托,它几乎做了同样的事情。一个属性可以将它的获取器和设置器调用传递给委托,委托可以代表属性本身处理这些调用。

你可能正在想,将获取器和设置器调用传递给委托有什么好处?只有你使用的委托才能回答这个问题。Kotlin 为大多数常见用例提供了多个预定义的标准委托。让我们看一下以下列表,其中包含可用的标准委托:

  • Delegates.notNull 函数和 lateinit

  • lazy 函数

  • Delegates.Observable 函数

  • Delegates.vetoble 函数

Delegates.notNull 函数和 lateinit

想象一个场景,你需要在一个类级别上声明一个属性,但你没有那里变量的初始值。你将在稍后某个时刻得到这个值,但在属性实际使用之前,你确信属性将在使用之前初始化,并且它不会是 null。但是,根据 Kotlin 语法,你必须初始化属性。快速修复方法是将其声明为 nullable var 属性,并分配一个默认的 null 值。但是,如我们之前提到的,由于你确信变量在使用时不会是 null,你不愿意将其声明为可空。

Delegates.notNull 函数就是为了在这种情况下帮助你。看看以下程序:

var notNullStr:String by Delegates.notNull<String>() 

fun main(args: Array<String>) { 
    notNullStr = "Initial value" 
    println(notNullStr) 
} 

关注第一行——var notNullStr:String by Delegates.notNull<String>(),我们声明了一个非空的 String var 属性,但没有初始化它。相反,我们写了 by Delegates.notNull<String>(),但这是什么意思?让我们检查一下。by 操作符是 Kotlin 中的一个保留关键字,用于与委托一起使用。by 操作符与两个操作数一起工作,by 的左侧将是需要委托的属性/类,而右侧将是委托。

委托——Delegates.notNull 允许你在不初始化属性的情况下暂时使用。它必须在使用之前初始化(就像我们在 main 方法的第一行所做的那样),否则它将抛出异常。

因此,让我们通过添加另一个属性来修改程序,我们将在使用之前不初始化它,看看会发生什么:

var notNullStr:String by Delegates.notNull<String>() 
var notInit:String by Delegates.notNull<String>() 

fun main(args: Array<String>) { 
    notNullStr = "Initial value" 
    println(notNullStr) 
    println(notInit) 
} 

输出看起来如下:

图片

因此,notInit 属性导致了异常——属性 notInit 应在使用前初始化

但是,变量声明——by Delegates.notNull()听起来不是很顺耳吗?Kotlin 团队也这样认为。这就是为什么从 Kotlin 1.1 开始,他们添加了一个简单的关键字——lateinit,以实现相同的目标。正如它简单说明了延迟初始化,它应该简单地是 lateinit

因此,让我们通过将 by Delegates.notNull() 替换为 lateinit 来修改最后一个程序。以下是修改后的程序:

lateinit var notNullStr1:String 
lateinit var notInit1:String 

fun main(args: Array<String>) { 
    notNullStr1 = "Initial value" 
    println(notNullStr1) 
    println(notInit1) 
} 

在这个程序中,我们必须重命名变量,因为你不能有两个同名的顶级(包级变量,没有任何类/函数)。除了变量名外,唯一改变的是我们添加了 lateinit,而不是 by Delegates.notNull()

因此,现在让我们看一下以下输出,以确定是否有任何变化:

图片

输出也是相同的,除了它稍微改变了错误信息。现在它说,lateinit 属性 notInit1 尚未初始化

懒加载函数

lateinit 关键字仅适用于 var 属性。Delegates.notNull() 函数也仅与 var 属性配合使用。

那么,当使用 val 属性时,我们应该怎么做呢?Kotlin 为你提供了另一个委托——lazy,它仅适用于 val 属性。但它的工作方式略有不同。

lateinitDelegates.notNull() 不同,你必须在声明变量时指定你想要如何初始化变量。那么,有什么好处呢?初始化将不会在变量实际使用之前调用。这就是为什么这个委托被称为 lazy;它允许属性的延迟初始化。

以下是一个代码示例:

val myLazyVal:String by lazy { 
    println("Just Initialised") 
    "My Lazy Val" 
} 

fun main(args: Array<String>) { 
    println("Not yet initialised") 
    println(myLazyVal) 
} 

因此,在这个程序中,我们声明了一个 String val 属性——myLazyVal,并使用(打印)了该属性在 main 函数的第二行。

现在,让我们专注于变量声明。lazy 委托接受一个 lambda,该 lambda 预期返回属性的值。

那么,让我们看看输出结果:

注意,输出清楚地显示,属性是在 main 方法的第一行执行后初始化的,即当属性实际上被使用时。这种属性的 lazy 初始化可以显著节省内存。在某些情况下,它也是一个方便的工具,例如,想象一下你想要使用其他属性/上下文来初始化属性,而这些属性/上下文只有在某个特定点之后才可用(但你已经有了属性名);在这种情况下,你可以简单地保持属性为 lazy,然后当确认初始化将成功时再使用它。

使用 Delegates.observable 观察属性值变化

委托不仅用于最近/延迟初始化属性。正如我们所学的,委托允许将属性的获取器和设置器调用转发到委托。这使得委托能够提供比最近/延迟初始化更多的酷炫功能。

这样一个酷炫的功能来自于 Delegates.observable。想象一下,你需要监视一个属性值的改变,并在这种改变发生时立即执行某些操作。我们首先想到的解决方案是重写设置器,但这会使代码显得很糟糕,并且使代码变得复杂,而委托正是为了拯救我们。

看看下面的例子:

var myStr:String by Delegates.observable("<Initial Value>") { 
    property, oldValue, newValue -> 
    println("Property `${property.name}` changed value from "$oldValue" to "$newValue"") 
} 

fun main(args: Array<String>) { 
    myStr = "Change Value" 
    myStr = "Change Value again" 
} 

这是一个简单的例子,我们使用 Delegates.observable 声明了一个 String 属性——myStr(我们将在查看输出后不久描述这种初始化),然后,在 main 函数内部,我们两次改变了 myStr 的值。

看看下面的输出:

在输出中,我们可以看到,每次我们更改值时,都会打印出属性的旧值和新值。这个程序中的 Delegates.observable 块负责输出中的日志。所以现在,让我们仔细看看 Delegates.observable 块,并了解它是如何工作的:

var myStr:String by Delegates.observable("<Initial Value>") { 
    property, oldValue, newValue -> 
    println("Property `${property.name}` changed value from "$oldValue" to "$newValue"") 
} 

Delegates.observable 函数接受两个参数来创建委托。第一个参数是属性的初始值,第二个参数是每当检测到值变化时应执行的 lambda。

Delegates.observable 的 lambda 预期有三个参数:

  • 第一个是一个 KProperty<out R> 的实例

KProperty 是 Kotlin stdlibkotlin.reflect 包中的一个接口,它是一个属性;例如一个命名的 valvar 声明。这个类的实例可以通过 :: 操作符获取。更多信息,请访问:kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-property/

  • 第二个参数包含属性的旧值(分配前的最后一个值)

  • 第三个参数是分配给属性的最新值(在分配中使用的新值)

因此,既然我们已经了解了Delegates.observable的概念,那么让我们继续使用一个新的委托,Delegates.vetoable

否决权 - Delegates.vetoable

Delegates.vetoable是另一个标准委托,它允许我们否决值的变化。

否决权,拉丁语为“我禁止”,是指单方面停止官方行动的权力(例如,由国家官员使用)。更多信息请参阅:en.wikipedia.org/wiki/Veto

这种否决权允许我们对属性的每一项分配进行逻辑检查,我们可以决定是否继续分配。

以下是一个示例:

var myIntEven:Int by Delegates.vetoable(0) { 
    property, oldValue, newValue -> 
    println("${property.name} $oldValue -> $newValue") 
    newValue%2==0 
} 

fun main(args: Array<String>) { 
    myIntEven = 6 
    myIntEven = 3 
    println("myIntEven:$myIntEven") 
} 

在这个程序中,我们创建了一个Int属性—myIntEven;这个属性应该只接受偶数作为分配。Delegates.vetoable委托的工作方式几乎与Delegates.observable函数相同,只是在 lambda 中有细微的变化。在这里,lambda 预期返回一个布尔值;如果返回的布尔值为true,则分配会被传递,否则分配会被拒绝。

回顾一下程序。当我们使用Delegates.vetoable委托声明变量时,我们传递了0作为初始值,然后在 lambda 中记录了一个分配调用,然后如果新值是偶数,我们将返回true,如果是奇数则返回false

这里是输出:

图片

因此,在输出中,我们可以看到两个分配日志,但当我们打印最后分配后的myIntEven属性时,我们可以看到最后的分配并没有成功。

有趣,不是吗?让我们看看Delegates.vetoable的另一个示例。看看以下代码:

var myCounter:Int by Delegates.vetoable(0) { 
    property, oldValue, newValue -> 
    println("${property.name} $oldValue -> $newValue") 
    newValue>oldValue 
} 

fun main(args: Array<String>) { 
    myCounter = 2 
    println("myCounter:$myCounter") 
    myCounter = 5 
    myCounter = 4 
    println("myCounter:$myCounter")  
    myCounter++ 
    myCounter-- 
    println("myCounter:$myCounter") 
} 

这个程序有一个属性—myCounter,它预期在每次分配时增加。

在 lambda 中,我们检查newValue值是否大于oldValue值。以下是输出:

图片

显示那些值增加的分配的输出是成功的,但那些值减少的分配被拒绝了。

即使当我们使用了递增和递减运算符时,递增运算符是成功的,但递减运算符却没有。没有委托,这个特性不会那么容易实现。

委托映射

因此,我们学习了如何使用标准委托,但 Kotlin 必须提供更多关于委托的功能。映射委托是委托带来的那些令人惊叹的功能之一。那么,它是什么呢?它是在函数/类构造函数中传递一个映射作为单个参数,而不是传递多个参数的自由。让我们看看。以下是一个应用映射委托的程序:

data class Book (val delegate:Map<String,Any?>) { 
    val name:String by delegate 
    val authors:String by delegate 
    val pageCount:Int by delegate 
    val publicationDate:Date by delegate 
    val publisher:String by delegate 
} 

fun main(args: Array<String>) { 
    val map1 = mapOf( 
            Pair("name","Reactive Programming in Kotlin"), 
            Pair("authors","Rivu Chakraborty"), 
            Pair("pageCount",400), 
            Pair("publicationDate",SimpleDateFormat("yyyy/mm/dd").parse("2017/12/05")), 
            Pair("publisher","Packt") 
    ) 
    val map2 = mapOf( 
            "name" to "Kotlin Blueprints", 
            "authors" to "Ashish Belagali, Hardik Trivedi, Akshay Chordiya", 
            "pageCount" to 250, 
            "publicationDate" to SimpleDateFormat("yyyy/mm/dd").parse("2017/12/05"), 
            "publisher" to "Packt" 
    ) 

    val book1 = Book(map1) 
    val book2 = Book(map2) 

    println("Book1 $book1 nBook2 $book2") 
} 

程序足够简单;我们定义了一个 Book 数据类,并在构造函数中,我们不是逐个取成员值,而是取一个映射,然后将所有值委托给映射代理。

这里需要注意的一点是,在映射中提到所有成员变量,并且键名应与属性名完全匹配。

这里是输出结果:

图片

简单吧?是的,代理就是这样强大。但你有没有好奇如果我们省略了映射中提到的任何属性会发生什么?它将简单地跳过你省略的属性,如果你明确尝试访问它们,那么它将抛出一个异常—java.util.NoSuchElementException

自定义代理

到目前为止,在本章中,我们已经看到了 Kotlin 中可用的标准代理。然而,Kotlin 允许我们编写自己的自定义代理,以满足我们的特定需求。

例如,在程序中,当我们使用 Delegates.vetoable 检查 Even 时,我们只能丢弃值赋值,但无法自动将下一个偶数赋值给变量。

在下面的程序中,我们使用了 makeEven,一个自定义代理,如果传递给赋值的数字是奇数,它会自动分配下一个偶数,否则如果传递给赋值的数字是偶数,它会传递那个数字。

看看下面的程序:

var myEven:Int by makeEven(0) { 
    property, oldValue, newValue, wasEven -> 
    println("${property.name} $oldValue -> $newValue, Even:$wasEven") 
} 

fun main(args: Array<String>) { 
    myEven = 6 
    println("myEven:$myEven") 
    myEven = 3 
    println("myEven:$myEven") 
    myEven = 5 
    println("myEven:$myEven") 
    myEven = 8 
    println("myEven:$myEven") 
} 

这里是输出结果:

图片

输出清楚地显示,每次我们将偶数分配给 myEven 时,它都会被分配,但当我们分配奇数时,下一个偶数(+1)会被分配。

对于这个代理,我们使用了与 Delegates.observable 几乎相同的 lambda 表达式,只是增加了一个额外的参数—wasEven:Boolean,如果分配的数字是偶数,则包含 true,否则包含 false

想知道我们是如何创建代理的吗?以下是代码:

abstract class MakeEven(initialValue: Int):ReadWriteProperty<Any?,Int> { 
    private var value:Int = initialValue 

    override fun getValue(thisRef: Any?, property: KProperty<*>) = value 

    override fun setValue(thisRef: Any?, property: KProperty<*>, newValue: Int) { 
        val oldValue = newValue 
        val wasEven = newValue %2==0 
        if(wasEven) { 
            this.value = newValue 
        } else { 
            this.value = newValue +1 
        } 
        afterAssignmentCall(property,oldValue, newValue,wasEven) 
    } 

    abstract fun afterAssignmentCall (property: KProperty<*>, oldValue: Int, newValue: Int, wasEven:Boolean):Unit 
} 

为了在 var 属性上创建代理,你需要实现 ReadWriteProperty 接口。

该接口有两个需要重写的方法—getValuesetValue。这些函数实际上是属性的获取器和设置器的委托函数。你可以从 getValue 函数返回你想要的价值,然后它将被转发为属性的返回值。每次访问属性时,都会调用 getValue 函数。同样,每次属性被分配值时,setValue 函数都会被调用,我们从 setValue 函数返回的任何内容实际上就是属性最终分配的值。例如,假设属性 a 被分配了 X,但从 setValue 函数返回了 Y,那么在赋值语句之后,属性 a 实际上会持有 Y 而不是 X

所以,如果你想从委托的 getValue 函数返回属性的值,你必须将属性的值存储在某个地方(是的,你将无法从原始属性中获取值,可能是因为原始属性甚至不会存储值,因为属性知道它将被委托)。在这个程序中,我们使用了一个可变的 var 属性——value,来存储属性的值。我们从 getValue 函数返回 value

setValue 函数内部,我们检查分配的 newValue 是否为偶数。如果是偶数,我们将 newValue 分配给 value 属性(它将来自 getValue 函数),如果 newValue 是奇数,我们将 newValue+1 分配给 value 属性。

MakeEven 类中,我们有一个抽象方法——afterAssignmentCall。我们在 setValue 函数的末尾调用了这个方法。这个方法是为了日志记录目的。

因此,委托几乎已经准备好了,但抽象方法怎么办?我们需要扩展这个类来应用委托,对吧?但记住我们使用它的代码,像 by makeEven(0) {...},所以那里必须有一个函数,不是吗?是的,有一个函数,以下是其定义:

 inline fun makeEven(initialValue: Int, crossinline onAssignment:(property: KProperty<*>, oldValue: Int, newValue: Int, wasEven:Boolean)->Unit):ReadWriteProperty<Any?, Int> 
        =object : MakeEven(initialValue){ 
    override fun afterAssignmentCall(property: KProperty<*>, oldValue: Int, newValue: Int, wasEven: Boolean) 
            = onAssignment(property,oldValue,newValue,wasEven) 
} 

我们创建了一个 MakeEven 的匿名对象,并将其作为委托传递,并将参数 lambda——onAssignment,作为抽象函数——afterAssignmentCall 传递。

所以,我们必须掌握委托,让我们继续前进,尝试一些关于委托的更有趣的方面。

局部委托

委托很强大,我们已经看到了这一点,但想想一个常见的情况,在方法内部我们声明并初始化一个属性,然后应用一个逻辑,这个逻辑要么使用属性,要么不使用它继续执行。例如,以下是这样的程序:

fun useDelegate(shouldPrint:Boolean) { 
    val localDelegate = "Delegate Used" 
    if(shouldPrint) { 
        println(localDelegate) 
    } 

    println("bye bye") 
} 

在这个程序中,我们将使用 localDelegate 属性,只有当 shouldPrint 的值为 true 时,否则我们不会使用它。但因为它被声明并初始化,所以它总是会占用内存空间。避免这种内存阻塞的一个选项是将属性放在 if 块内部,但这是一个简单的示例程序,在这里我们可以轻松地将变量声明移动到 if 块内部,而在许多现实场景中,将变量声明移动到 if 块内部是不可能的。

那么,解决方案是什么?是的,使用 lazy 委托可以拯救我们的生命。但在 Kotlin 1.1 之前,这是不可能的。

所以,以下是一个更新的程序:

fun useDelegate(shouldPrint:Boolean) { 
    val localDelegate by lazy { 
        "Delegate Used" 
    } 
    if(shouldPrint) { 
        println(localDelegate) 
    } 

    println("bye bye") 
} 

尽管我们在这个例子中只使用了 lazy,但从 Kotlin 1.1 开始,我们可以在局部属性中应用任何委托。

类委托

类委托是 Kotlin 的另一个有趣特性。如何?只需想想以下情况。

你有一个接口,I,以及两个类,ABAB 都实现了 I 接口。在你的代码中,你有一个 A 的实例,并且想要从这个 A 实例创建一个 B 的实例。

在传统的继承中,这是不可能直接实现的;你必须编写一大堆糟糕的代码才能达到这个目的,但类委托(class delegation)就是为了解决这个问题而存在的。

下面是相应的代码:

interface Person { 
    fun printName() 
} 

class PersonImpl(val name:String):Person { 
    override fun printName() { 
        println(name) 
    } 
} 

class User(val person:Person):Person by person { 
    override fun printName() { 
        println("Printing Name:") 
        person.printName() 
    } 
} 

fun main(args: Array<String>) { 
    val person = PersonImpl("Mario Arias") 
    person.printName() 
    println() 
    val user = User(person) 
    user.printName() 
} 

在这个程序中,我们创建了 User 的实例,其中包含其成员属性——person,它是一个 Person 接口的实例。在主函数中,我们将 PersonImpl 的实例传递给 user 以创建 User 的实例。

现在,让我们看看 User 的声明。在颜色(:)之后,短语 Person by person 表示类 User 继承自 Person 类,并且期望从提供的 person 实例复制 Person 的行为。

下面是输出结果:

输出显示了覆盖(overriding)工作如预期进行,我们还可以访问 person 的属性和函数,就像访问一个普通属性一样。

真的是一个很棒的功能,不是吗?

摘要

在本章中,我们学习了代理(delegates),并看到了如何以各种方式使用代理来使我们的代码高效且整洁。我们学习了代理的不同特性和组成部分,以及如何使用它们。

下一章将介绍协程,这是 Kotlin 的一个开创性特性,它能够在保持开发者生活简单直接的同时,实现无缝的异步处理。

所以,不要等待太久,现在就开始下一章吧。

第七章:使用协程进行异步编程

今天的软件开发格局使得异步处理成为最重要的主题之一。处理器和核心数量的不断增长以及对外部服务的巨大消耗(近年来随着微服务架构的采用而增长)是我们应该关注并努力采用良好异步方法的因素之一。

Kotlin 对协程的实现是构建异步应用程序的优秀工具。

在本章中,我们将涵盖以下主题:

  • 协程

  • 替代方法

  • 异步处理

  • 通道和演员

协程简介

让我们从没有协程的简单例子开始:

import kotlin.concurrent.thread

fun main(args: Array<String>) {
   thread {
      Thread.sleep(1000)
      println("World!")
   }
   print("Hello ")
   Thread.sleep(2000)
}

thread 函数在不同的线程中执行一段代码。在代码块内部,我们使用 Thread.sleep 模拟一个昂贵的 I/O 计算(例如通过 HTTP 从微服务访问数据)。Thread.sleep 将阻塞当前线程,直到传递的参数指定的毫秒数。在这个例子中,我们不等待计算完成,而是继续做其他事情;当其他计算正在执行时,我们打印另一条消息,"Hello"。最后,我们等待两秒钟,直到计算完成。

这段代码并不美观,我们可以做得更好:

fun main(args: Array<String>) {
   val computation = thread {
      Thread.sleep(1000)
      println("World!")
   }
   print("Hello ")
   computation.join()
}

在这个版本中,我们有一个名为 computation 的线程引用;在最后,我们等待 join() 方法完成。这比仅仅等待固定的时间更聪明,因为现实生活中的计算可能具有不同的执行时间。

理解 JVM 线程

线程是 JVM(以及其他平台)上异步并发应用程序的构建块。大多数情况下,JVM 线程由硬件线程(如处理器内部的内核)支持。硬件线程可以支持多个软件线程(JVM 线程是一种软件线程),但在任何给定时间只能执行一个软件线程。

操作系统(或 JVM)决定在每个硬件线程上执行哪个软件线程,并在活动线程之间快速切换,从而给人一种似乎有多个软件线程同时执行的感觉,但实际上活跃的软件线程数量与硬件线程数量相同。但是,在大多数情况下,认为所有软件线程都在同时执行是有用的。

JVM 中的线程非常快且响应迅速,但它们也有代价。每个 Thread 在创建、销毁(当垃圾回收时)和上下文切换(当线程成为执行线程或停止执行时存储和恢复线程状态的过程)时都会消耗 CPU 时间和内存。由于这种成本相对较高,JVM 应用程序不能有大量的线程。

在典型的开发机器上,一个 JVM 应用程序可以轻松地处理 100 个线程:

fun main(args: Array<String>) {
   val threads = List(100){
      thread {
         Thread.sleep(1000)
         print('.')
      }
   }
   threads.forEach(Thread::join)
}

如果您使用任何外部应用程序来监控 JVM 应用程序,例如 VisualVM 或 JConsole(以及其他),您将看到如下图形:

图片

我们可以将线程数增加到 1,000,如下面的截图所示:

图片

内存容量正在以较快的速度增长,达到 1.5 GB 以上。

我们能否将线程数增加到 10,000?请看以下截图:

图片

答案是明确的否定;当应用程序因OutOfMemoryError而死亡时,创建了大约 2,020 个线程(此应用程序使用默认设置运行;这些设置可以在启动时更改)。

让我们尝试 1,900,这是一个安全的执行量的合理估计:

图片

是的,我们可以运行 1,900 个并发线程。

在现代 JVM 应用程序中,创建和销毁线程被视为一种不良做法;相反,我们使用Executor,这是一个抽象,允许我们管理和重用线程,从而降低创建和销毁的成本:

import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

fun main(args: Array<String>){
   val executor = Executors.newFixedThreadPool(1024)
   repeat(10000){
      executor.submit {
         Thread.sleep(1000)
         print('.')
      }
   }
   executor.shutdown() 
}

我们创建了一个executor值,其内部有一个最多 1,024 个线程的线程池。然后,我们提交 10,000 个任务;最后,我们关闭Executor。当我们关闭Executor时,它不能接受新的任务并执行所有挂起的任务,如下所示:

图片

有许多选项可以微调和玩耍,例如Executor的线程数和池的类型或其实际实现。

关于 JVM 线程的理论远比本书所能涵盖的多。如果您想阅读和学习更多关于线程和并发的知识,我们推荐经典书籍《Java 并发实践(2006)》由 Dough Lea、David Holmes、Joseph Bower、Joshua Block、Tim Peierls 和 Brian Goetz 所著,由 Addison-Wesley Professional 出版。我们还推荐 Venkat Subramanian 所著的《Programming Concurrency on the JVM(2011)》由 Pragmatic Bookshelf 出版,以及 Douglas Schmidt 所著的《Java Concurrency LiveLessons(2015)》视频,由 Addison-Wesley Professional 提供。最后但同样重要的是,我们建议 Javier Fernández Gonzáles 所著的《Java Concurrency》系列书籍和视频,由 Packt 出版。

嗨,协程世界!

现在,让我们用协程重写我们的Hello World应用程序。

但是,嘿!什么是协程?基本上,协程是一个非常轻量级的线程,它运行一段代码,具有类似的生命周期,但可以带有返回值或异常完成。技术上讲,协程是可挂起计算的一个实例,这种计算可能会挂起。协程不绑定到特定的线程,可以在一个Thread中挂起并在另一个中恢复执行:

import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking

fun main(args: Array<String>) = runBlocking {
    launch {
        delay(1000)
        println("World")
    }
    print("Hello ")
    delay(2000)
}

这里有一些需要讨论的内容:

  • runBlocking:此函数创建一个协程并阻塞当前Thread,直到协程完成,返回其结果值(在这种情况下为Unit)。

  • launch: 这个函数创建一个新的协程而不阻塞当前 Thread,并返回 Job(在此处忽略)。

  • delay: 这个函数是一个挂起函数(稍后会有更多介绍),它延迟当前协程的执行而不阻塞当前线程。

  • suspend: 一个挂起函数是一个可能挂起协程执行而不阻塞当前 Thread 的函数;因此,挂起函数必须在协程内部调用——它不能从普通代码中调用。该函数必须带有 suspend 修饰符。因此,delay 可以在 runBlockinglaunch 中调用,这两个函数(以及其他函数)将挂起 lambda 作为最后一个参数——挂起 lambda 是一个带有 suspend 修饰符的 lambda。

让我们在继续之前总结一下我们现在所知道的内容,以及一些其他概念:

概念 描述
协程 一个非常轻的线程,可以返回一个值,并且可以挂起和恢复。
挂起函数 一个带有 suspend 修饰符的函数。它可以挂起协程而不阻塞线程。挂起函数必须在协程内部调用,例如 delay
挂起 lambda 一个带有 suspend 修饰符的 lambda 函数。它可以挂起协程而不阻塞线程。
协程构建器 一个接受一个挂起 lambda 的函数,创建一个协程,可能返回一个结果,例如 runBlocking
挂起点 调用挂起函数的点。
延续 在挂起点处挂起的协程的状态,它代表了挂起点之后其执行剩余部分的状态。

让我们回到正事上来。

正如我们之前讨论的,计算可能具有不同的执行时间。因此,delay 在我们的 Hello World 示例中并不理想:

fun main(args: Array<String>) = runBlocking {
   val job = launch {
      delay(1000)
      println("World")
   }
   print("Hello ")
   job.join()
}

就像我们的线程示例一样,我们获取由 launch 创建的作业的引用,并使用挂起函数 join 在最后挂起它。

到目前为止,一切顺利。但协程真的这么轻吗?我们能拥有 10,000 个协程吗?

让我们通过执行以下代码片段来试一试:

fun main(args: Array<String>) = runBlocking {
      val jobs = List(10000) {
         launch {
            delay(1000)
            print('.')
         }
      }
      jobs.forEach { job -> job.join() }
   }
}

哦,确实如此!它工作了:

它们比 Executor 解决方案快得多,内存少得多,线程少得多(仅有大约七个线程),而且最重要的是,它们非常容易阅读。

让我们用一百万个协程来试试:

不到 2,000 个线程需要超过 1.5 GB 的内存。一百万个协程需要不到 700 MB 的内存——我的论点就此结束。结论是协程非常非常轻。

在现实生活中使用协程

微基准测试非常有趣,它们让我们了解了 Kotlin 协程的强大之处,但它们并不代表真实场景。

让我们介绍我们的真实场景:

enum class Gender {
   MALE, FEMALE;

   companion object {
      fun valueOfIgnoreCase(name: String): Gender = valueOf(name.toUpperCase())
   }
}

typealias UserId = Int

data class User(val id: UserId, val firstName: String, val lastName: String, val gender: Gender)

data class Fact(val id: Int, val value: String, val user: User? = null)

interface UserService {
   fun getFact(id: UserId): Fact
}

我们的 UserService 接口只有一个方法——getFact 将返回有关我们用户的 Chuck Norris 风格的事实,该用户由用户 ID 标识。

实现应该首先在本地数据库中检查用户;如果用户不在数据库中,它应该从RandomUser API服务(randomuser.me/documentation)获取它,然后存储以供将来使用。一旦服务有了用户,它应该再次在数据库中检查与该用户相关的信息;如果信息不在数据库中,它应该从Internet Chuck Norris Database API服务(www.icndb.com/api/)获取,并将其存储在数据库中。一旦服务有了信息,就可以返回。服务必须尝试减少外部调用(数据库、API 服务)的数量,而不使用缓存。

现在,让我们介绍其他接口,HTTP 客户端——UserClientFactClient

interface UserClient {
   fun getUser(id: UserId): User
}

interface FactClient {
   fun getFact(user: User): Fact
}

我们将使用http4kwww.http4k.org/)实现客户端的 HTTP 通信,并使用 Kotson(github.com/SalomonBrys/Kotson)进行 JSON 处理。这两个库都是为 Kotlin 设计的,但任何其他库都应该工作得很好:

import com.github.salomonbrys.kotson.*
import com.google.gson.GsonBuilder
import org.http4k.client.ApacheClient

abstract class WebClient {
   protected val apacheClient = ApacheClient()

   protected val gson = GsonBuilder()
         .registerTypeAdapter<User> {
            deserialize { des ->
               val json = des.json
               User(json["info"]["seed"].int,
                     json["results"][0]["name"]["first"].string.capitalize(),
                     json["results"][0]["name"]["last"].string.capitalize(),
                     Gender.valueOfIgnoreCase(json["results"][0]["gender"].string))

            }
         }
         .registerTypeAdapter<Fact> {
            deserialize { des ->
               val json = des.json
               Fact(json["value"]["id"].int,
                     json["value"]["joke"].string)
            }
         }.create()!!
}

两个客户端都将扩展一个包含http4k ApacheClient和用 Kotson DSL 配置的Gson值的公共父类:

import org.http4k.core.Method
import org.http4k.core.Request

class Http4KUserClient : WebClient(), UserClient {
   override fun getUser(id: UserId): User {
      return gson.fromJson(apacheClient(Request(Method.GET, "https://randomuser.me/api")
            .query("seed", id.toString()))
            .bodyString())
   }
}

Http4KUserClient非常简单,这两个库都很容易使用,我们将大量代码移动到父类:

class Http4KFactClient : WebClient(), FactClient {
   override fun getFact(user: User): Fact {
      return gson.fromJson<Fact>(apacheClient(Request(Method.GET, "http://api.icndb.com/jokes/random")
            .query("firstName", user.firstName)
            .query("lastName", user.lastName))
            .bodyString())
            .copy(user = user)
   }
}

Http4KFactClient使用copy方法在Fact实例中设置用户值。

这些类实现得非常好,但为了测试我们算法的实际性能,我们将模拟这些接口:

class MockUserClient : UserClient {
   override fun getUser(id: UserId): User {
      println("MockUserClient.getUser")
      Thread.sleep(500)
      return User(id, "Foo", "Bar", Gender.FEMALE)
   }
}

class MockFactClient : FactClient {
   override fun getFact(user: User): Fact {
      println("MockFactClient.getFact")
      Thread.sleep(500)
      return Fact(Random().nextInt(), "FACT ${user.firstName}, ${user.lastName}", user)
   }
}

看一下以下数据库存储库,UserRepositoryFactRepository

interface UserRepository {
   fun getUserById(id: UserId): User?
   fun insertUser(user: User)
}

interface FactRepository {
   fun getFactByUserId(id: UserId): Fact?
   fun insertFact(fact: Fact)
}

对于我们的存储库,我们将使用 Spring 5 的JdbcTemplate。Spring 5 提供了对 Kotlin 的支持,包括用于简化 Kotlin 使用的扩展函数(你可以在任何应用程序中使用JdbcTemplate,它不需要是 Spring 应用程序):

import org.springframework.dao.EmptyResultDataAccessException
import org.springframework.jdbc.core.JdbcTemplate

abstract class JdbcRepository(protected val template: JdbcTemplate) {
   protected fun <T> toNullable(block: () -> T): T? {
      return try {
         block()
      } catch (_: EmptyResultDataAccessException) {
         null
      }
   }
}

与客户端一样,两个存储库也将有一个父类——在这种情况下,有一个将EmptyResultDataAccessException(Spring 表示不存在的记录的方式)转换为可空——符合 Kotlin 语法的函数;

两个实现都很直接,如下所示:

import org.springframework.jdbc.core.queryForObject

class JdbcUserRepository(template: JdbcTemplate) : JdbcRepository(template), UserRepository {
   override fun getUserById(id: UserId): User? {
      return toNullable {
         template.queryForObject("select * from USERS where id = ?", id) { resultSet, _ ->
            with(resultSet) {
               User(getInt("ID"),
                     getString("FIRST_NAME"),
                     getString("LAST_NAME"),
                     Gender.valueOfIgnoreCase(getString("GENDER")))
            }
         }
      }
   }

   override fun insertUser(user: User) {
      template.update("INSERT INTO USERS VALUES (?,?,?,?)",
            user.id,
            user.firstName,
            user.lastName,
            user.gender.name)
   }
}

class JdbcFactRepository(template: JdbcTemplate) : JdbcRepository(template), FactRepository {
   override fun getFactByUserId(id: Int): Fact? {
      return toNullable {
         template.queryForObject("select * from USERS as U inner join FACTS as F on U.ID = F.USER where U.ID = ?", id) { resultSet, _ ->
            with(resultSet) {
               Fact(getInt(5),
                     getString(6),
                     User(getInt(1),
                           getString(2),
                           getString(3),
                           Gender.valueOfIgnoreCase(getString(4))))
            }
         }
      }
   }

   override fun insertFact(fact: Fact) {
      template.update("INSERT INTO FACTS VALUES (?,?,?)", fact.id, fact.value, fact.user?.id)
   }
}

对于我们的数据库,我们使用的是 H2 内存数据库,但任何数据库都可以工作(你可以使这个应用程序与一些不同的持久化机制一起工作,例如 NoSQL 数据库或任何缓存):

fun initJdbcTemplate(): JdbcTemplate {
   return JdbcTemplate(JdbcDataSource()
         .apply {
            setUrl("jdbc:h2:mem:facts_app;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false")
         })
         .apply {
            execute("CREATE TABLE USERS (ID INT AUTO_INCREMENT PRIMARY KEY, FIRST_NAME VARCHAR(64) NOT NULL, LAST_NAME VARCHAR(64) NOT NULL, GENDER VARCHAR(8) NOT NULL);")
            execute("CREATE TABLE FACTS (ID INT AUTO_INCREMENT PRIMARY KEY, VALUE_ TEXT NOT NULL, USER INT NOT NULL,  FOREIGN KEY (USER) REFERENCES USERS(ID) ON DELETE RESTRICT)")
         }
}

函数initJdbcTemplate使用 H2 DataSource创建JdbcTemplate,一旦准备就绪,它就在apply扩展函数内部创建表。apply扩展函数用于配置属性和调用初始化代码,并返回相同的值:

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

与客户端一样,为了测试,我们将使用模拟:

class MockUserRepository : UserRepository {
   private val users = hashMapOf<UserId, User>()

   override fun getUserById(id: UserId): User? {
      println("MockUserRepository.getUserById")
      Thread.sleep(200)
      return users[id]
   }

   override fun insertUser(user: User) {
      println("MockUserRepository.insertUser")
      Thread.sleep(200)
      users[user.id] = user
   }
}

class MockFactRepository : FactRepository {

   private val facts = hashMapOf<UserId, Fact>()

   override fun getFactByUserId(id: UserId): Fact? {
      println("MockFactRepository.getFactByUserId")
      Thread.sleep(200)
      return facts[id]
   }

   override fun insertFact(fact: Fact) {
      println("MockFactRepository.insertFact")
      Thread.sleep(200)
      facts[fact.user?.id ?: 0] = fact
   }

}

使用这些模拟,我们的最坏情况场景大约是 1,600 毫秒:

  • UserRepository.getUserById = 200ms ~

  • UserClient.getUser = 500ms ~

  • UserRepository = 200ms ~

  • FactClient.getFact = 500ms ~

  • FactRepository.insertRepository = 200ms ~

现在,我们将使用不同的异步风格实现UserService,包括一个同步实现,这是我们的基础。

同步实现

同步代码易于编写,可预测,且易于测试,但在某些情况下,它并没有以最佳方式使用系统资源:

class SynchronousUserService(private val userClient: UserClient,
                      private val factClient: FactClient,
                      private val userRepository: UserRepository,
                      private val factRepository: FactRepository) : UserService {

   override fun getFact(id: UserId): Fact {
      val user = userRepository.getUserById(id)
      return if (user == null) {
         val userFromService = userClient.getUser(id)
         userRepository.insertUser(userFromService)
         getFact(userFromService)
      } else {
         factRepository.getFactByUserId(id) ?: getFact(user)
      }
   }

   private fun getFact(user: User): Fact {
      val fact = factClient.getFact(user)
      factRepository.insertFact(fact)
      return fact
   }
}

这里没有什么花哨的,只是你正常的、老套的代码:

fun main(args: Array<String>) {

   fun execute(userService: UserService, id: Int) {
         val (fact, time) = inTime {
            userService.getFact(id)
         }
         println("fact = $fact")
         println("time = $time ms.")
      }

   val userClient = MockUserClient()
   val factClient = MockFactClient()
   val userRepository = MockUserRepository()
   val factRepository = MockFactRepository()

   val userService = SynchronousUserService(userClient,
         factClient,
         userRepository,
         factRepository)

   execute(userService, 1)
   execute(userService, 2)
   execute(userService, 1)
   execute(userService, 2)
   execute(userService, 3)
   execute(userService, 4)
   execute(userService, 5)
   execute(userService, 10)
   execute(userService, 100)   

}

我们执行UserService.getFact方法 10 次以预热 JVM(JVM 优化使得应用程序在一段时间后运行得更快)。不用说,执行时间是 1,600 毫秒,这里没有惊喜。

回调

异步代码的一种流行风格是在单独的线程中执行代码,当上述线程完成其执行时调用callback函数。回调风格的一个缺点是我们的异步函数现在需要一个额外的参数。在 Kotlin 的 lambda 支持下,回调风格在 Kotlin 中很容易编写。

对于我们的回调实现,我们需要为我们的客户端和仓库提供适配器:

import kotlin.concurrent.thread

class CallbackUserClient(private val client: UserClient) {
   fun getUser(id: Int, callback: (User) -> Unit) {
      thread {
         callback(client.getUser(id))
      }
   }
}

class CallbackFactClient(private val client: FactClient) {
   fun get(user: User, callback: (Fact) -> Unit) {
      thread {
         callback(client.getFact(user))
      }
   }
}

class CallbackUserRepository(private val userRepository: UserRepository) {
   fun getUserById(id: UserId, callback: (User?) -> Unit) {
      thread {
         callback(userRepository.getUserById(id))
      }
   }

   fun insertUser(user: User, callback: () -> Unit) {
      thread {
         userRepository.insertUser(user)
         callback()
      }

   }
}

class CallbackFactRepository(private val factRepository: FactRepository) {
   fun getFactByUserId(id: Int, callback: (Fact?) -> Unit) {
      thread {
         callback(factRepository.getFactByUserId(id))
      }
   }

   fun insertFact(fact: Fact, callback: () -> Unit) {
      thread {
         factRepository.insertFact(fact)
         callback()
      }
   }
}

这些适配器在单独的线程中执行我们的代码,并在完成后调用回调,lambda:

class CallbackUserService(private val userClient: CallbackUserClient,
                    private val factClient: CallbackFactClient,
                    private val userRepository: CallbackUserRepository,
                    private val factRepository: CallbackFactRepository) : UserService {

   override fun getFact(id: UserId): Fact {
      var aux: Fact? = null
      userRepository.getUserById(id) { user ->
         if (user == null) {
            userClient.getUser(id) { userFromClient ->
               userRepository.insertUser(userFromClient) {}
               factClient.get(userFromClient) { fact ->
                  factRepository.insertFact(fact) {}
                  aux = fact
               }

            }
         } else {
            factRepository.getFactByUserId(id) { fact ->
               if (fact == null) {
                  factClient.get(user) { factFromClient ->
                     factRepository.insertFact(factFromClient) {}
                     aux = factFromClient
                  }
               } else {
                  aux = fact
               }
            }
         }
      }
      while (aux == null) {
         Thread.sleep(2)
      }
      return aux!!
   }
}

回调风格往往非常晦涩难懂;当多个回调嵌套时,情况更糟(在社区中亲切地称为回调地狱)。结尾的while循环中使用Thread.sleep看起来非常笨拙。它的执行速度也非常快,耗时 1,200 毫秒,但创建了大量的线程,内存消耗与之相匹配。

每个函数调用创建一个线程的回调实现将很快在生产场景中消耗掉应用程序的所有资源;因此,它应该基于某种Executor实现或类似的东西。

Java 未来

由于回调风格难以维护,近年来出现了其他风格。其中一种风格是未来。未来是指可能在将来完成的计算。当我们调用Future.get方法时,它将获取其结果,但我们也会阻塞线程:

import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class FutureUserService(private val userClient: UserClient,
                  private val factClient: FactClient,
                  private val userRepository: UserRepository,
                  private val factRepository: FactRepository) : UserService {
   override fun getFact(id: UserId): Fact {

      val executor = Executors.newFixedThreadPool(2)

      val user = executor.submit<User?> { userRepository.getUserById(id) }.get()
      return if (user == null) {
         val userFromService = executor.submit<User> { userClient.getUser(id) }.get()
         executor.submit { userRepository.insertUser(userFromService) }
         getFact(userFromService, executor)
      } else {
         executor.submit<Fact> {
            factRepository.getFactByUserId(id) ?: getFact(user, executor)
         }.get()
      }.also {
         executor.shutdown()
      }
   }

   private fun getFact(user: User, executor: ExecutorService): Fact {
      val fact = executor.submit<Fact> { factClient.getFact(user) }.get()
      executor.submit { factRepository.insertFact(fact) }
      return fact
   }
}

使用未来的实现与我们的同步实现非常相似,但到处都是那些奇怪的submitget函数。我们还有一个需要关注的Executor。总时间大约是 1,200 毫秒,创建了大量的线程,比回调示例中的更多。一个可能的选项是每个实例或全局地有一个Executor,但在这种情况下,我们还需要有某种方式来管理其生命周期。

Kovenant 中的承诺

编写异步代码的另一种选择是使用承诺。承诺与未来类似(在许多框架中,未来和承诺是同义的),因为它代表了一个可能在将来完成的计算。我们有一个阻塞方法来获取其结果,但我们也可以以回调风格对其结果做出反应。

Kovenant (kovenant.komponents.nl/) 是 Kotlin 中对承诺的实现:

import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.task
import nl.komponents.kovenant.then

class PromiseUserService(private val userClient: UserClient,
                   private val factClient: FactClient,
                   private val userRepository: UserRepository,
                   private val factRepository: FactRepository) : UserService {

   override fun getFact(id: UserId): Fact {

      return (task {
         userRepository.getUserById(id)
      } then { user ->
         if (user == null) {
            task {
               userClient.getUser(id)
            } success  { userFromService ->
               userRepository.insertUser(userFromService)
            } then { userFromService ->
               getFact(userFromService).get()
            }
         } else {
            task { factRepository.getFactByUserId(id) ?: getFact(user).get() }
         }
      }).get().get()
   }

   private fun getFact(user: User): Promise<Fact, Exception> = task {
      factClient.getFact(user)
   } success  { fact ->
      factRepository.insertFact(fact)
   }
}

函数 task 创建 Promise<T, Exception>(在我们之前的其他实现中未涉及)。我们可以以几种方式与 Promise<T, Exception> 交互:

  • get(): T:这会阻塞当前线程并返回 promise 的结果。

  • then(bind: (T) -> R): Promise<R, Exception>:这与函数式集合上的 map 类似;它返回一个具有新类型的新 Promise 值。

  • success(callback: (T) -> Unit): Promise<T, Exception>:这是在 Promise 执行成功时的回调。它对于副作用很有用。

  • fail(callback: (Exception) -> Unit): Promise<T, Exception>:这是在失败时的回调,类似于 catch 块。

  • always(callback: () -> Unit): Promise<T, Exception>:这总是执行,就像 finally 块一样。

代码一开始看起来很难理解,但一旦习惯了 promise 习惯用法,阅读起来就很容易。此外,请注意,promise 是一个未来,因此你可以编写类似于我们未来示例的代码,但不需要与 Executors 打扰。Java 8 包含一种名为 CompletableFuture<T> 的新类型未来,它可以被视为一个 promise。

首次执行(Kovenant 初始化阶段)的执行时间约为 1,350 毫秒,然后稳定在 1,200 毫秒左右。在默认配置下,Kovenant 尽可能使用尽可能多的线程,导致内存使用量很高,但 Kovenant 可以微调以使用更少的线程。

协程

现在,让我们用协程重新整理我们的示例:

import kotlinx.coroutines.experimental.Deferred
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking

class CoroutineUserService(private val userClient: UserClient,
                     private val factClient: FactClient,
                     private val userRepository: UserRepository,
                     private val factRepository: FactRepository) : UserService {
   override fun getFact(id: UserId): Fact = runBlocking {
      val user = async { userRepository.getUserById(id) }.await()
      if (user == null) {
         val userFromService = async { userClient.getUser(id) }.await()
         launch { userRepository.insertUser(userFromService) }
         getFact(userFromService)
      } else {
         async { factRepository.getFactByUserId(id) ?: getFact(user) }.await()
      }
   }

   private suspend fun getFact(user: User):Fact {
      val fact: Deferred<Fact> = async { factClient.getFact(user) }
      launch { factRepository.insertFact(fact.await()) }
      return fact.await()
   }
}

我们代码的简洁性比我们的 Future 示例更接近,几乎接近我们的同步代码。我们在上一节中介绍了 runBlockinglaunch,但在这里引入了一个新的协程构建器,async

async 协程构建器接收一段代码块并异步执行它,返回 Deferred<T>Deferred 是一个带有 await 方法的 Future,它会在协程完成前阻塞协程但不会阻塞线程;Deferred 还从 Job 继承,因此继承了所有它的方法,例如 join

协程代码感觉自然,当我们使用异步代码时又很明确,但由于资源消耗低,我们可以在代码中使用尽可能多的协程;例如,CoroutineUserService 比其他任何实现使用的线程和内存都少一半。

现在我们有了所有实现,我们可以比较代码复杂度和资源消耗:

代码复杂度 资源消耗
同步 代码复杂度非常低。 资源消耗非常低,但性能较慢。
回调 需要非常高的适配器;预期会有重复;嵌套回调难以阅读;并且有各种技巧。 资源消耗很高。使用共享 Executor 可以提高效率,但会增加代码复杂度。
Futures 代码复杂度中等。Executorsget() 很吵,但仍然可读。 资源消耗高,但可以通过不同的 Executor 实现和共享执行器进行微调,但这会增加代码复杂度。
承诺 使用承诺风格(thensuccess)时,代码复杂度中等。使用 futures 风格(get),它可以像协程一样流畅,而不会影响性能。 资源消耗非常高,具有顶级性能,但可以通过不更改代码的方式进行微调。
协程 代码复杂度低;与同步风格相同的大小,具有显式的异步操作块。 资源消耗低,具有出色的性能。

总体而言,协程是明显的赢家,Kovenant 的承诺紧随其后。

协程上下文

协程总是在上下文中运行。所有协程构建器默认指定上下文,并且该上下文可以通过协程体内的 coroutineContext 值访问:

import kotlinx.coroutines.experimental.*

fun main(args: Array<String>) = runBlocking {
   println("run blocking coroutineContext = $coroutineContext")
   println("coroutineContext[Job] = ${coroutineContext[Job]}")
   println(Thread.currentThread().name)
   println("-----")

   val jobs = listOf(
         launch {
            println("launch coroutineContext = $coroutineContext")
            println("coroutineContext[Job] = ${coroutineContext[Job]}")
            println(Thread.currentThread().name)
            println("-----")
         },
         async {
            println("async coroutineContext = $coroutineContext")
            println("coroutineContext[Job] = ${coroutineContext[Job]}")
            println(Thread.currentThread().name)
            println("-----")
         },
         launch(CommonPool) {
            println("common launch coroutineContext = $coroutineContext")
            println("coroutineContext[Job] = ${coroutineContext[Job]}")
            println(Thread.currentThread().name)
            println("-----")
         },
         launch(coroutineContext) {
            println("inherit launch coroutineContext = $coroutineContext")
            println("coroutineContext[Job] = ${coroutineContext[Job]}")
            println(Thread.currentThread().name)
            println("-----")
         }
   )

   jobs.forEach { job ->
      println("job = $job")
      job.join()
   }
}

每个协程上下文还包括 CoroutineDispatcher,它决定了协程在哪个线程上运行。例如,asynclaunch 这样的协程构建器默认使用 DefaultDispatcher 分派器(在当前协程版本 0.2.1 中,DefaultDispatcher 等于 CommonPool;然而,这种行为在未来可能会改变)。

协程上下文还可以持有值;例如,你可以通过使用 coroutineContext[Job] 来恢复协程的工作。

协程上下文可以用来控制其子协程。我们的 100 万个协程示例可以被重新设计,以便将所有子协程连接起来:

fun main(args: Array<String>) = runBlocking {

   val job = launch {
      repeat(1_000_000) {
         launch(coroutineContext) {
            delay(1000)
            print('.')
         }
      }
   }

   job.join()
}

而不是每个百万个协程都有自己的上下文,我们可以设置一个共享的协程上下文,它实际上来自外部的 launch 协程上下文。当我们连接外部的 launch 作业时,它也会连接所有其协程子作业。

通道

两个协程之间通信(或者协程与外部世界通信,如 async)的一种方式是通过 Deferred<T>

import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking

fun main(args: Array<String>) = runBlocking {
    val result = CompletableDeferred<String>()

   val world = launch {
      delay(500)
      result.complete("World (from another coroutine)")
   }

   val hello =launch {
      println("Hello ${result.await()}")
   }

   hello.join()
   world.join()
}

对于单个值,延迟调用(Deferreds)是可行的,但有时我们希望发送一个序列或流。在这种情况下,我们可以使用 ChannelChannel 类似于 BlockingQueue,但使用的是挂起操作而不是阻塞操作,而且 Channel 可以被 close

import kotlinx.coroutines.experimental.channels.*

fun main(args: Array<String>) = runBlocking<Unit> {
   val channel = Channel<String>()

   val world = launch {
      delay(500)
      channel.send("World (from another coroutine using a channel)")
   }

   val hello = launch {
      println("Hello ${channel.receive()}")
   }

   hello.join()
   world.join()
}

让我们用通道的方式编写我们的 100 万个协程示例,如下所示:

fun main(args: Array<String>) = runBlocking<Unit> {

   val channel = Channel<Char>()

   val jobs = List(1_000_000) {
      launch {
         delay(1000)
         channel.send('.')
      }
   }

   repeat(1_000_000) {
      print(channel.receive())
   }

   jobs.forEach { job -> job.join() }
}

当然,这不是通道的预期用途。通常,单个协程(或多个)向通道发送消息:

fun main(args: Array<String>) = runBlocking<Unit> {

   val channel = Channel<Char>()

   val sender = launch {
      repeat(1000) {
         delay(10)
         channel.send('.')
         delay(10)
         channel.send(',')
      }
      channel.close()
   }

   for (msg in channel) {
      print(msg)
   }

   sender.join()

}

通道本身就是一个迭代器,因此它可以在 for 循环中使用。

编写此代码的一个更简单的方法是使用 produce 构建器,如下所示:

fun dotsAndCommas(size: Int) = produce {
   repeat(size) {
      delay(10)
      send('.')
      delay(10)
      send(',')
   }
}

fun main(args: Array<String>) = runBlocking<Unit> {
   val channel = dotsAndCommas(1000)

   for (msg in channel) {
      print(msg)
   }
}

produce 构建器返回 ReceiveChannel<T>,这是一种仅用于接收的通道类型。Channel<T> 同时扩展了 SendChannel<T>ReceiveChannel<T> 这两种类型。

通道管道

当我们有通道时,我们可以有相关的模式,例如管道。一个 pipeline 是一系列连接消费者和生产者的通道,类似于 Unix 管道或 企业集成模式EIP)。

让我们使用 EIPs 编写自己的销售系统。首先,让我们看看模型:

data class Quote(val value: Double, val client: String, val item: String, val quantity: Int)

data class Bill(val value: Double, val client: String)

data class PickingOrder(val item: String, val quantity: Int)

现在,让我们看看模式:

import kotlinx.coroutines.experimental.CoroutineContext

fun calculatePriceTransformer(coroutineContext: CoroutineContext, quoteChannel: ReceiveChannel<Quote>) = produce(coroutineContext) {
   for (quote in quoteChannel) {
      send(Bill(quote.value * quote.quantity, quote.client) to PickingOrder(quote.item, quote.quantity))
   }
}

calculatePriceTransformer 函数从通道接收报价并将其转换为 Pair<Bill, PickingOrder>

fun cheapBillFilter(coroutineContext: CoroutineContext, billChannel: ReceiveChannel<Pair<Bill, PickingOrder>>) = produce(coroutineContext) {
   billChannel.consumeEach { (bill, order) ->
      if (bill.value >= 100) {
         send(bill to order)
      } else {
         println("Discarded bill $bill")
      }
   }
}

cheapBillFilter 函数很好地过滤了低于 100bill 值:

suspend fun splitter(filteredChannel: ReceiveChannel<Pair<Bill, PickingOrder>>,
                accountingChannel: SendChannel<Bill>,
                warehouseChannel: SendChannel<PickingOrder>) = launch {
   filteredChannel.consumeEach { (bill, order) ->
      accountingChannel.send(bill)
      warehouseChannel.send(order)
   }
}

splitterPair<Bill, PickingOrder> 分割成各自的通道:

suspend fun accountingEndpoint(accountingChannel: ReceiveChannel<Bill>) = launch {
   accountingChannel.consumeEach { bill ->
      println("Processing bill = $bill")
   }
}

suspend fun warehouseEndpoint(warehouseChannel: ReceiveChannel<PickingOrder>) = launch {
   warehouseChannel.consumeEach { order ->
      println("Processing order = $order")
   }
}

accountingEndpointwarehouseEndpoint 都通过打印处理它们各自的消息,但在实际场景中,我们可以将这些消息存储到数据库中,发送电子邮件或使用 JMSAMQPKafka 向其他系统发送消息:

fun main(args: Array<String>) = runBlocking {

   val quoteChannel = Channel<Quote>()
   val accountingChannel = Channel<Bill>()
   val warehouseChannel = Channel<PickingOrder>()

   val transformerChannel = calculatePriceTransformer(coroutineContext, quoteChannel)

   val filteredChannel = cheapBillFilter(coroutineContext, transformerChannel)

   splitter(filteredChannel, accountingChannel, warehouseChannel)

   warehouseEndpoint(warehouseChannel)

   accountingEndpoint(accountingChannel)

   launch(coroutineContext) {
      quoteChannel.send(Quote(20.0, "Foo", "Shoes", 1))
      quoteChannel.send(Quote(20.0, "Bar", "Shoes", 200))
      quoteChannel.send(Quote(2000.0, "Foo", "Motorbike", 1))
   }

   delay(1000)
   coroutineContext.cancelChildren()
}

main 方法组装我们的销售系统并对其进行测试。

可以使用协程通道实现许多其他通道消息模式,例如扇入、扇出和 actors。我们将在下一节中介绍 actors

管理可变状态

当我们处理异步代码时,主要关注点(以及噩梦的来源)是如何处理可变状态。我们在第三章不可变性 - 它很重要中介绍了如何使用函数式风格减少可变状态。但有时使用函数式不可变风格是不可能的。协程为此问题提供了一些替代方案。

在以下示例中,我们将使用几个协程来更新一个计数器:

import kotlin.system.measureTimeMillis

suspend fun repeatInParallel(times: Int, block: suspend () -> Unit) {
   val job = launch {
      repeat(times) {
         launch(coroutineContext) {
            block()
         }
      }
   }
   job.join()
}

fun main(args: Array<String>) = runBlocking {
   var counter = 0

   val time = measureTimeMillis {
      repeatInParallel(1_000_000) {
         counter++
      }
   }
   println("counter = $counter")
   println("time = $time")
}

对于较小的数字,counter 是正确的,但一旦我们开始增加大小,我们就会看到奇怪的数字。

现在,我们可以看看协程为我们提供的替代方案。

切换上下文

我们的第一种选择是使用不同的上下文来更新操作:

import kotlinx.coroutines.experimental.*

fun main(args: Array<String>) = runBlocking {
   var counter = 0

   val counterContext = newSingleThreadContext("CounterContext")

   val time = measureTimeMillis {
      repeatInParallel(1_000_000) {
         withContext(counterContext) {
            counter++
         }
      }
   }
   println("counter = $counter")
   println("time = $time")
}

withContext 函数在特定的协程上下文中执行一个块——在这种情况下,是单线程的。切换上下文是一种强大的技术,它让我们能够以细粒度的方式操纵代码的运行方式。

线程安全结构

从 Java 5 开始,我们可以访问一些原子线程安全的结构,这些结构在协程中仍然很有用:

import java.util.concurrent.atomic.AtomicInteger

fun main(args: Array<String>) = runBlocking {
   val counter = AtomicInteger(0)

   val time = measureTimeMillis {
      repeatInParallel(1_000_000) {
         counter.incrementAndGet()
      }
   }
   println("counter = ${counter.get()}")
   println("time = $time")
}

AtomicInteger 给我们许多线程安全的原子操作。还有更多线程安全的结构,如其他原子原语和并发集合。

互斥锁

一个 Mutex(互斥锁)对象允许多个协程访问相同的资源,但不能同时访问:

import kotilnx.coroutines.experimental.sync.Mutex
import kotlinx.coroutines.experimental.sync.withLock

fun main(args: Array<String>) = runBlocking {
   val mutex = Mutex()
   var counter = 0

   val time = measureTimeMillis {
      repeatInParallel(1_000_000) {
         mutex.withLock {
            counter++
         }
      }
   }
   println("counter = $counter")
   println("time = $time")
}

Mutex 对象的工作方式与同步控制结构类似,但它不是阻塞线程,而是阻塞协程。

Actors

actor 是一种与其它 actor 和外部世界通过消息交互的对象。一个 actor 对象可以有一个私有的内部可变状态,该状态可以通过消息修改和访问,但不能直接访问。由于它们一致的编程模型,演员在近年来越来越受欢迎,并在多百万用户的应用程序中成功进行了测试,例如用 Erlang 编写的 WhatsApp,这种语言使演员成为焦点:

import kotlinx.coroutines.experimental.channels.actor

sealed class CounterMsg
object IncCounter : CounterMsg()
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg()

fun counterActor(start:Int) = actor<CounterMsg> {
   var counter = start
   for (msg in channel) {
      when (msg) {
         is IncCounter -> counter++
         is GetCounter -> msg.response.complete(counter)
      }
   }
}

要编写一个 actor,首先,我们需要定义我们想要发送哪些消息。在这里,我们创建了两个消息,IncCounterGetCounterGetCounter 有一个 CompletableDeferred<Int> 值,这将让我们知道 actor 外部的计数器值。

我们可以使用 actor<CounterMsg> 构建器来创建 actor。在我们的 actor 协程内部,我们可以访问 channel 属性,即 ReceiveChannel<CounterMsg>,以接收消息并对它们做出反应。counterActor(Int) 函数将返回 SendChannel<CounterMsg>;因此,我们只能调用 send(CounterMsg)close() 这两个函数:

fun main(args: Array<String>) = runBlocking {
   val counterActor = counterActor(0)

   val time = measureTimeMillis {
      repeatInParallel(1_000_000) {
         counterActor.send(IncCounter)
      }
   }

   val counter = CompletableDeferred<Int>()
   counterActor.send(GetCounter(counter))
   println("counter = ${counter.await()}")
   println("time = $time")
}

最初,演员(Actors)可能难以理解,但一旦你理解了,actor 模型对于创建复杂和强大的系统来说非常直接。

在本书的示例代码中,你可以找到一个使用 actors 实现的 UserService 示例。你可以在网上观看它:github.com/MarioAriasC/FunctionalKotlin/blob/master/Chapter07/src/main/kotlin/com/packtpub/functionalkotlin/chapter07/facts.kt#L377

摘要

协程显示出改变我们思考异步代码和执行方式的高潜力。在本章中,我们介绍了如何编写协程以及如何使用协程上下文和通道。我们还全面探讨了如何处理异步共享可变状态。

在我们下一章中,我们将学习函数式集合及其操作。

第八章:Kotlin 中的集合和数据操作

在前面的章节中,我们已经涵盖了广泛的主题,从 Kotlin 中的数据类型、类和对象开始,到上一章中的不可变性、函数、委托和协程。在本章中,我们将讨论 Kotlin 中的集合框架和数据操作。Kotlin 从 Java 继承了集合框架,但在函数式编程方面与之有显著的不同。

Kotlin 提供的集合框架比 Java 更函数式,作为 Kotlin 的签名,它更容易使用和理解。

我们将从这个章节的集合基础知识开始,然后逐渐过渡到 Kotlin 中集合支持的数据操作。以下是本章将要涵盖的主题列表:

  • 集合简介

  • IteratorIterable 接口

  • Kotlin 中的集合类型—ArrayListMapSet

  • 可变性和不可变性

  • 与列表一起工作

  • 各种数据操作—mapsortfilterflatMappartitionfoldgroup by

那么,我们还在等什么呢?让我们开始学习集合。

集合简介

集合框架是一组类和接口,它提供了一种统一的架构来执行常见的数据相关操作,如下所示:

  • 搜索

  • 排序

  • 插入

  • 删除

  • 操作

我们在日常生活中使用的所有列表、映射和集合都是这个集合框架的一部分。

所有集合框架都包含以下内容:

  • 接口:这些是抽象数据类型,用于表示集合。接口允许独立于其表示的细节来操作集合。在面向对象的语言中,这些通常形成层次结构。

  • 实现:这些是实现接口集合的具体实现。本质上,这些是可重用的数据结构。

  • 算法:执行有用计算的方法,例如搜索和排序(如前所述),在实现集合接口的对象上。这些算法被称为 多态。同一个方法可以用于许多不同实现的相关集合接口。简而言之,算法是可重用的功能。

除了 Java 和 Kotlin 集合框架之外,最著名的集合框架示例是 C++ 标准模板库STL)和 Smalltalk 的集合层次结构。

集合框架的优势

那么,拥有集合框架有什么好处呢?有几个好处,但最重要的是,它减少了编程时间和努力。集合框架为开发者提供了高质量(在性能和代码优化方面)的有用数据结构和算法的实现,同时提供了与无关 API 之间的互操作性。您可以在程序中使用这些实现,从而减少编程努力和时间。

因此,既然我们已经了解了集合框架是什么,那么现在让我们来看看集合框架中类和接口的层次结构。

那么,让我们看一下以下图:

图片

正如我们之前提到的,集合框架是一组数据类型和类,它使我们能够处理一组(或几组)数据。这组数据可能是一个简单的列表/映射/集合,或者任何其他数据结构。

上述图表示了 Kotlin 的集合框架。就像 Java 一样,Kotlin 中的所有集合接口都起源于Iterable接口。然而,Kotlin 的集合框架与 Java 的不同;Kotlin 区分可变和不可变集合。

Kotlin 有两个基本集合接口,即IterableMutableIterableIterable接口被Collection接口扩展,该接口定义了基本的只读集合操作(如sizeisEmpty()contains()等)。

MutableCollection接口扩展了Collection接口和MutableIterable接口,增加了读写特性。

在 Java 引入集合框架之前,开发者通常使用数组、向量以及哈希表来处理一组数据。这种方法的缺点是它们都没有一些共同的方法。因此,集合框架被创建出来,通过提供跨各种类型集合的通用方法和操作,使开发者的生活变得更简单。

集合框架是在 Kotlin 语言形成之前在 Java 中引入的,并且从一开始就被包含在 Kotlin 中。

您难道不好奇为什么有这么多集合类型吗?让我们在接下来的章节中介绍它们时,找出一些最常用集合类型的目的。

List 和 MutableList

List 是使用最广泛的集合数据类型之一。它是一个Collection接口的实现,用于处理一组有序数据。

列表中的数据可以根据添加的顺序进行排序(例如,如果我们将3添加到Int List中,那么4将出现在列表中,在3之前,就像数组一样)或者甚至可以根据其他排序算法进行排序。

正如我们之前提到的,Kotlin 区分可变和只读集合类型;因此,只读的List接口只包含只读函数,如下所示:

  • fun get(index: Int):E: 此方法用于从给定索引的列表中获取元素。

  • fun indexOf(element: @UnsafeVariance E):Int: 此方法用于识别列表中元素的索引。此方法将在整个列表中搜索指定的元素,如果它在列表中,则返回元素的位位置。否则,它将返回-1

  • fun listIterator(): ListIterator<E>: 如果您想获取ListIterator的实例(这将在本章后面讨论,当我们讨论IteratorIterable时)。

  • fun subList(fromIndex: Int, toIndex: Int): List<E>: 返回具有指定fromIndextoIndex值的列表的一部分。

考虑到这一点,它只包含只读函数,我们如何有一个包含数据的列表?虽然您不能在创建后向不可变列表中添加数据,但您当然可以创建一个预填充数据的不可变列表(显然,否则拥有不可变列表就没有任何意义了)。您可以通过多种方式实现这一点,但最流行的方式是使用listOf函数。

listOf函数声明如下(可以在Collections.kt中的kotlin.collections包内找到):

public fun <T> listOf(vararg elements: T): List<T> 

正如我们在函数声明中看到的,该函数接受一个泛型类型的vararg参数作为元素;函数将返回一个包含这些元素的list实例。正如您已经知道的,vararg参数的重要性在于它可以包含 0 到几乎 64K 个参数(如果每个参数是 1 字节,一个函数可以有最大 64K 字节的分配,所以实际上会更少);因此,在用listOf函数创建list时,您甚至可以不传递任何参数来创建一个空列表,或者传递尽可能多的参数(假设您不需要超过 64K 字节)来创建包含这些参数的只读list

以下程序是listOf函数的一个示例:

fun main(args: Array<String>) { 
 val list = listOf<Int>(1,2,3,4,5,6,7,8,9,10) 

    for (i in list) { 
        println("Item $i") 
    } 
} 

在前面的程序中,我们创建了一个包含数字110list值。然后我们使用for循环遍历list值中的每个元素并打印它。

让我们看一下以下输出以验证这一点:

图片

for循环花括号内的i in list告诉for循环遍历list值中的所有元素,并将每个迭代中的元素复制到临时变量i

我们将在本章后面部分探讨更多与集合操作的方法,但首先让我们学习不同类型的集合。

因此,继续我们关于列表的讨论,我们已经看到了如何使用预定义元素创建不可变列表;现在,我们将探讨如何创建和操作可变列表,但在那之前,让我们看看创建空列表的方法。

让我们逐一分析以下程序:

fun main(args: Array<String>) { 
 val emptyList1 = listOf<Any>() val emptyList2 = emptyList<Any>() 

    println("emptyList1.size = ${emptyList1.size}") 
    println("emptyList2.size = ${emptyList2.size}") 
} 

因此,在前面的程序中,我们创建了空列表,一个使用不带参数的listOf函数,另一个使用emptyList函数。请注意,如果listOf函数不带任何参数调用,则它内部调用emptyList函数。

以下是输出截图:

图片

因此,我们已经看到了如何使用预定义的一组元素与不可变列表一起工作,但如果我们需要动态地向list值中添加项目怎么办?Kotlin 为此提供了可变列表。

以下示例将帮助您理解不可变列表:

fun main(args: Array<String>) { 
 val list = mutableListOf(1,2,4)//(1) 

    for (i in list) { 
        println("for1 item $i") 
    } 

    println("-----Adding Items-----") 

 list.add(5)//(2) list.add(2,3)//(3) list.add(6)//(4) 

    for (i in list) { 
        println("for2 item $i") 
    } 
} 

以下是程序的输出:

图片

现在,让我们解释一下程序。首先,我们在注释(1)处使用mutableListOf函数创建了list值,其中包含项目124。请注意,这里我们跳过了类型参数,如果您将元素传递给函数,则这不是很重要,因为 Kotlin 有类型推断。我们在添加项目之前打印了list值。

对于listOf或其他任何集合函数,类型推断是一个问题。因此,如果您传递了元素或已提供集合的类型,则不需要指定正在使用的集合的泛型类型。

在注释(2)处,我们使用List$add()函数向list中添加了项目5,该函数将提供的项目追加到list数组中。

然后,在注释(3)处,我们使用带有索引参数的add函数将项目4添加到第二个位置(从0开始计数,如通常一样)。

然后,我们又用5list数组中添加了元素。

因此,我们在list数组中添加了元素,并通过for循环访问所有项目,但如何访问单个元素呢?让我们通过以下示例来了解如何在 Kotlin 中访问和修改单个元素。请看以下示例:

fun main(args: Array<String>) { 
    val list = listOf( 
            "1st Item", 
            "2nd Item", 
            "3rd Item", 
            "4th Item", 
            "5th Item" 
    ) 

    println("3rd Item on the list - ${list.get(2)}") 
    println("4rd Item on the list - ${list[3]}") 
} 

我们使用索引2访问了第三个元素,使用索引3访问了第四个元素。原因很简单,因为在数组和列表中,计数从0开始。

这里要注意的事情是,Kotlin 为列表提供了开箱即用的支持,并提供了一个方括号运算符([])来访问list值的元素,就像数组一样。在第一个get语句中,我们使用带有索引的get函数来获取该索引的元素;在第二个get语句中,我们使用了方括号,它反过来调用那个get函数。

由于列表按顺序/索引存储项目,因此很容易通过索引从列表中获取项目;如果您只想从列表中获取特定元素并且知道该元素的索引,则可以轻松跳过循环。只需将元素传递给get函数,您就有该元素了。

通过索引获取元素不支持其他集合接口,如set(尽管OrderedSet支持它们),它不支持元素的排序。

因此,既然我们已经对列表有了些许了解,让我们继续前进,看看集合。

集合和 MutableSet

就像 List 一样,Set 在 Kotlin 中也有以下两种变体:

  • Set

  • MutableSet

Set 是只读的,而 MutableSetSet 的可变版本,它包含读写功能。

就像列表一样,集合的值也有只读函数和属性,如 sizeiterator() 等。我们在这里省略了它们的提及,以避免在这本书中重复内容。请注意,集合不像列表那样进行排序(除非你使用 OrderedSet)。因此,它缺少涉及排序的函数,如 indexOf(item)add(index, item) 等。

集合中的集合表示数学集合(如集合论中的集合)。

以下是一个使用 MutableSet 的示例:

fun main(args: Array<String>) { 
    val set = mutableSetOf(1,2,3,3,2) 

    println("set $set") 

    set.add(4) 
    set.add(5) 
    set.add(5) 
    set.add(6) 

    println("set $set") 
} 

以下是输出:

输出清楚地显示,尽管我们在初始化和之后都向 set 中添加了多个重复项,但只有唯一的项被插入,所有重复的项都被忽略了。

现在,你可能很好奇,这与自定义类和数据类会发生什么;让我们通过以下示例来检查:

data class MyDataClass (val someNumericValue:Int, val someStringValue:String)
class MyCustomClass (val someNumericValue:Int, val someStringValue:String) {
    override fun toString(): String {
      return "MyCustomClass(someNumericValue=$someNumericValue, someStringValue=$someStringValue)"
    }
  }
fun main(args: Array<String>) {
    val dataClassSet = setOf(
         MyDataClass(1,"1st obj"),
         MyDataClass(2,"2nd obj"),
         MyDataClass(3,"3rd obj"),
         MyDataClass(2,"2nd obj"),
         MyDataClass(4,"4th obj"),
         MyDataClass(5,"5th obj"),
         MyDataClass(2,"will be added"),
         MyDataClass(3,"3rd obj")
    )
    println("Printing items of dataClassSet one by one")
    for(item in dataClassSet) {
      println(item)
    }
    val customClassSet = setOf(
      MyCustomClass(1,"1st obj"),
      MyCustomClass(2,"2nd obj"),
      MyCustomClass(3,"3rd obj"),
      MyCustomClass(2,"2nd obj"),
      MyCustomClass(4,"4th obj"),
      MyCustomClass(5,"5th obj"),
      MyCustomClass(5,"5th Obj"),
      MyCustomClass(3,"3rd obj")
    )
    println("Printing items of customClassSet one by one")
    for(item in customClassSet) {
      println(item)
    }
 }

在这个程序中,我们首先创建了一个数据类和一个自定义类,然后使用它们创建了集合并插入了重复项。

让我们查看以下输出以检查集合是否没有重复项:

仔细查看前面的输出。虽然与数据类的情况一样,set 忽略了重复项,但在尝试与普通类做同样的事情时,它无法检测到重复插入并保留了它们。

dataClassSet 中最后添加的项是 MyDataClass(2,"will be added"),如果你认为它是一个重复项,那么请再次检查,尽管这个对象的 someNumericValue 值与之前的相同,但 someStringValue 值与之前对象的 someStringValue 不同。

为什么这是一个异常?答案是简单明了——集合框架在向 set 值添加项时内部使用 hashCode()equals() 函数来执行相等性检查,而自定义类中缺少这些函数。

在 Kotlin 中,编译器会自动提取 hashCode()equals() 函数。因此,set 值能够区分重复项,而无需自定义这些函数的实现。有关数据类的更多信息,请访问以下链接:kotlinlang.org/docs/reference/data-classes.html

因此,如果我们实现这些函数,那么 set 也将能够区分 customClassSet 值中的重复项。显然,这对于数据类也是这样工作的。只需将以下代码添加到 MyCustomClass 定义中并运行程序,自己看看效果:

override fun hashCode() = someStringValue.hashCode()+someNumericValue.hashCode() 

    override fun equals(other: Any?): Boolean { 
        return other is MyCustomClass && other.someNumericValue == someNumericValue && other.someStringValue==someStringValue 
    } 

很酷,不是吗?所以,我们已经完成了ListSet。现在让我们看看Map接口;然后,我们将讨论集合框架提供的数据操作函数。

Map 和 MutableMap

集合框架中的Map接口与其他所有我们之前覆盖的接口略有不同;与其他接口不同,它使用键值对。不,这并不类似于PairPair只是两个值组合在一起,而映射是一组键值对。

在映射中,键是唯一的,不能重复。如果你添加两个具有相同键的值,那么后面的值将替换前面的值。另一方面,值可以是冗余的/重复的。这种行为背后的原因是,在映射中,值是根据其键存储和检索的,因此冗余的键将使得无法区分它们,也无法获取它们的值。

Kotlin 中Map的声明读起来像接口Map<K, out V>K值是键的泛型类型,而V是值的泛型类型。

要了解更多关于集合的信息,让我们看看一些函数和属性。查看以下列表:

  • val size: Int: 这个函数表示Map接口的大小,即映射中驻留的键值对数量。

  • fun isEmpty(): Boolean: 这个函数有助于检查Map接口是否为空。

  • fun containsKey(key: K): Boolean: 这个函数检查提供的key是否在它拥有的键值对集合中,如果找到则返回true

  • operator fun get(key: K): V?: 这个函数兼操作符(如果像数组一样使用方括号[])返回与键对应的值,如果键不存在则返回 null。

  • val keys: Set<K>: 这个函数表示在该映射的某个时间点可用的键集合。由于键不能重复且无序,Set值是存储它们的最佳数据结构。

  • val values: Collection<V>: 包含map值的所有值作为一个集合。

  • interface Entry<out K, out V>: 这个函数定义在Map接口内部。Entry表示Map接口中的单个键值对。键值对作为条目存储在map值中。

  • val entries: Set<Map.Entry<K, V>>: 这个函数可以获取映射中的所有条目。

之前的都是只读接口的Map,因为它只支持只读操作。对于读写访问,你必须使用mutableMap函数。因此,现在让我们看看mutableMap提供的读写接口,如下所示:

  • fun put(key: K, value: V): V? : 这个接口向Map添加键值对,并返回与键关联的先前值(如果有,则为 null,如果键之前不在Map中)。

  • fun remove(key: K): V?:此接口从 Map 接口中删除具有键的键值对,并返回值。如果键不存在于 Map 接口中,则返回 null。

  • fun putAll(from: Map<out K, V>): Unit:此接口从提供的 map 值添加键值对。

  • fun clear(): Unit:正如其名所示,此实例清除 map 值。它移除 map 值包含的每一项——每个键和每个值。

因此,既然我们现在知道了 Map 接口提供的接口和函数,让我们现在通过一个 Map 的例子来展示。

让我们通过以下示例进行说明:

fun main(args: Array<String>) { 
    val map = mapOf( 
            "One".to(1), 
            "Two".to(2), 
            "Three".to(3), 
            "Four".to(4), 
            "Five".to(0),//(1) We placed 0 instead of 5 here, will be replaced later 
            "Six".to(6), 
            "Five".to(5)//(2) This will replace earlier map of "Five".to(0) 
            ) 

    println("The value at Key `Four` is ${map["Four"]}") 

    println("Contents in map") 
    for(entry in map) { 
        println("Key ${entry.key}, Value ${entry.value}") 
    } 

    val mutableMap = mutableMapOf<Int,String>() 

    mutableMap.put(1,"Item 1") 
    mutableMap.put(2,"Item 2") 
    mutableMap.put(3,"Item 3") 
    mutableMap.put(4,"Item 4") 

    println("Replacing value at key 1 - ${mutableMap.put(1,"Item 5")}")//(3) 

    println("Contents in mutableMap") 
    for(entry in mutableMap) { 
        println("Key ${entry.key}, Value ${entry.value}") 
    } 
} 

因此,我们展示了以下两种类型映射的使用:

  • 只读 Map

  • 可读写 MutableMap

Kotlin 为你提供了一个接受 Pair 类型的 vararg 参数的 mapOf() 函数版本。这使得你可以轻松地创建只读映射——只需将键值对作为 Pair 实例传递给 mapOf() 函数。

在进一步检查和讨论程序之前,让我们看看输出。请看以下截图:

在创建地图时,在注释 (1) 中,我们传递了一个 "Five".to(0) 对,在注释 (2) 中,我们传递了 "Five".to(5) 对到同一个 mapOf 函数,以检查 map"Five" 键存储的值;输出表明 map 采用了第二个值——5,正如我们之前所描述的,map 的值总是取相同键的最后一个值。

还要注意,Kotlin 还支持在 Map 中使用类似数组的方括号。你可以传递键而不是索引。

因此,当我们熟悉了 Kotlin 集合框架中的三个最重要的接口:ListSetMap 后,现在让我们继续前进,了解集合中的数据操作。

集合中的数据操作

Kotlin 为其集合框架提供了开箱即用的支持。因此,Kotlin 的集合框架充满了有趣的功能,使其与其他语言的集合框架(如 Java)区别开来。你已经接触到了其中的一些功能,例如为只读和可变集合提供单独的接口、类似方框操作符的数组等。我现在要介绍的是 Kotlin 集合框架中最有趣的功能之一,但通常被忽视——数据操作函数。

Kotlin 支持其所有集合框架接口、对象和类的数据操作函数。数据操作函数是指我们可以通过它们访问、处理或操作集合中的数据的操作符和函数;如果你熟悉 ReactiveX 框架/RxJava/RxKotlin,你会发现它们很相似,因为 Kotlin 主要从那里借鉴了它们。

下面是我们将要介绍的一些集合数据操作函数列表:

  • map 函数

  • filter 函数

  • flatMap 函数

  • drop 函数

  • take函数

  • zip函数

那么,我们还在等什么呢?让我们开始吧。

尽管如此,使用集合的数据操作函数会让你感觉像在使用流/Rx,但它们在本质上与流/Rx 没有任何相似之处。它们所做的只是使用高阶函数和扩展函数为你提供类似流的接口,并且在内部它们操作在相同的循环中(是的,你读对了,它们使用循环来产生结果,然后像简单的帝国程序一样从函数中返回它)。建议你在程序中避免使用这些函数的大链,因为最终你会得到多个循环。在这种情况下使用forEach或你自己的循环是更好的选择,因为你可以使用forEach或你自己的循环在一个循环中执行多个操作。然而,对于单个操作或小的链,你当然可以使用这些函数来使你的代码更有组织性。

地图函数

map函数允许你将算法应用于整个集合,并作为结果集获得结果。这对于使你的代码更有组织性和编写循环很有帮助(尽管它会在内部使用循环,但你将免于编写那些样板代码)。

map函数接收集合中的所有元素作为每次迭代的元素,并应返回应放置在结果列表中替代传递项的计算结果项。

以下是一个示例:

fun main(args: Array<String>) { 
    val list = listOf<Int>(1,2,3,4,5,6,7,8,9,10) 
    val modifiedList = list.map { it*2 } 

    println("modifiedList -> $modifiedList") 
} 

因此,我们有一个Int列表,我们需要将列表中的每个项乘以2,我们只需一行代码就能轻松完成——list.map { it*2 },这通常需要我们多两到三行样板代码。疯狂,不是吗?

以下是程序的输出:

图片

如预期的那样,map函数将提供的 lambda 函数应用于列表的每个元素,并返回结果列表。

filter函数

想象一下你需要过滤集合中的项的情况。例如,当你想从整数列表中获取仅偶数时。filter函数就是为了帮助你在这些场景中。

filter函数接收集合中的所有元素作为每次迭代的元素,并应根据其确定是否应将传递的项放在结果列表中来返回truefalse

以下是一个程序示例:

fun main(args: Array<String>) { 
    val list = 1.until(50).toList()//(1) 
    val filteredListEven = list.filter { it%2==0 }//(2) 

    println("filteredListEven -> $filteredListEven") 

    val filteredListPSquare = list.filter { 
        val sqroot = sqrt(it.toDouble()).roundToInt() 
        sqroot*sqroot==it 
    }//(3) 

    println("filteredListPSquare -> $filteredListPSquare") 
} 

在这个程序中,我们首先使用IntRange帮助获取一个包含从150数字的Int列表。然后,我们在注释(2)中过滤列表以获取偶数并打印它们。在注释(3)中,我们过滤包含从150Int值的原始列表以获取完全平方数并打印它们。

以下是程序的输出:

图片

之前的代码片段及其输出显示了这些数据操作函数可以消除多少样板代码。

平滑映射函数

集合框架中可用的另一个令人惊叹的函数是flatMap函数。

就像map函数一样,它将集合中的每个项目作为迭代接收,但与map函数不同,它应该为传递的每个项目返回另一个集合。然后,这些返回的集合被组合起来创建结果集合。

看看以下示例:

fun main(args: Array<String>) { 
    val list = listOf(10,20,30) 

    val flatMappedList = list.flatMap { 
        it.rangeTo(it+2).toList() 
    } 

    println("flatMappedList -> $flatMappedList") 
} 

输出看起来如下:

图片

尽管原始列表中只包含三个数字——102030,但结果列表中每个原始列表中的数字都增加了三个数字,这都要归功于flatMap函数。

删除函数

可能存在一些场景,当你想要丢弃集合的一部分(比如说,前 5 个或最后 10 个)并处理剩余的部分时。Kotlin 的集合框架为你提供了一组drop函数,这些函数可以帮助你在这些场景下。看看以下程序:

fun main(args: Array<String>) { 
    val list = 1.until(50).toList() 

    println("list.drop(25) -> ${list.drop(25)}")//(1) 
    println("list.dropLast(25) -> ${list.dropLast(25)}")//(2) 
} 

在前面的程序中,我们在注释(1)中从列表中删除了前25个项目,在注释(2)中,我删除了最后25个项目。

以下截图显示了程序的输出:

图片

工作得完美,不是吗?

take函数

take函数与drop函数的工作方式正好相反。你可以从集合中选择一部分,忽略其余部分。

看看以下程序:

fun main(args: Array<String>) { 
    val list = 1.until(50).toList() 

    println("list.take(25) -> ${list.take(25)}")//(1) 
    println("list.takeLast(25) -> ${list.takeLast(25)}")//(2) 
    println("list.takeWhile { it<=10 } -> ${list.takeWhile { it<=10 }}")//(3) 
    println("list.takeLastWhile { it>=40 } -> ${list.takeLastWhile { it>=40 }}")//(4) 
} 

而在注释(1)和注释(2)中的 while 语句与之前的drop函数相反,它们只是从列表中取出并打印25个项目。

注释(3)中的语句有点不同,这里我们使用了takeWhile函数。takeWhile函数接受一个谓词,并在谓词返回true时继续在结果集合中取项目;一旦谓词返回falsetakeWhile值将停止检查更多项目,并返回结果集合。

takeLastWhile值的工作方式与它相反。

以下是一个输出截图:

图片

现在我们继续使用zip函数。

zip函数

zip函数确实如其名所示,它将集合“压缩”在一起。令人困惑?让我们看看以下示例:

fun main(args: Array<String>) { 
    val list1 = listOf(1,2,3,4,5) 
    val list2 = listOf( 
            "Item 1", 
            "Item 2", 
            "Item 3", 
            "Item 4", 
            "Item 5" 
    ) 

    val resultantList = list1.zip(list2) 

    println(resultantList) 
} 

我们创建了两张列表——一张是Int类型的,另一张是String类型的。然后我们通过将Int列表与String列表进行zip操作创建了一个结果列表,并打印了这个结果列表。

那么,resultantList值包含什么?zip函数执行了什么操作?

让我们通过查看以下输出自行决定:

图片

太棒了,不是吗?zip函数接受另一个集合,将源集合与提供的集合组合起来,并为每个项目创建一个Pair值。但如果集合的项目数量不同怎么办?如果我们想将列表中的每个项目与同一列表中的下一个项目组合起来呢?

让我们再举一个例子。看看以下代码:

fun main(args: Array<String>) { 
    val list1 = listOf(1,2,3,4,5,6,7,8) 
    val list2 = listOf( 
            "Item 1", 
            "Item 2", 
            "Item 3", 
            "Item 4", 
            "Item 5" 
    ) 

    println("list1.zip(list2)-> ${list1.zip(list2)}") 

    println("list1.zipWithNext() -> ${list1.zipWithNext()}") 
} 

因此,这里的第一个println语句回答了我们的第一个问题——它试图将两个项目数量不对称的列表组合起来。

在第二个println语句中,我们使用了zipWithNext函数,该函数将集合中的一个项目与同一集合的下一个项目进行压缩。那么,让我们看一下输出结果,以了解发生了什么。

以下为输出结果:

图片

因此,zip操作符仅将list1中的那些项目与list2中的配对项进行压缩,并跳过了剩余的项。另一方面,zipWithNext操作符则按预期工作。

因此,我们已经完成了 Kotlin 集合框架中的数据操作函数。然而,Kotlin 为您提供了更多关于集合的能力;所以,让我们继续前进,看看它还能提供什么。

分组集合

Kotlin 的集合框架允许您根据需求对集合进行分组。例如,如果您有一个字符串列表,并希望根据它们的长度进行分组,您可以使用groupBy函数轻松实现,该函数根据提供的逻辑对集合进行分组,并返回包含该组集合的Map

因此,以下是一个简短的示例:

fun main(args: Array<String>) { 
    val list = 1.rangeTo(50).toList() 

    println(list.groupBy { it%5 }) 
} 

因此,我们在这里所做的是:我们创建了一个包含从150(包括两端)的数字的Int列表,然后我们尝试根据它们除以5的余数来分组它们。

因此,应该有五个组,从05,每个组都应该包含 10 个数字。让我们检查以下输出结果,看看这是否发生了。

图片

因此,groupBy函数按预期工作,并返回包含分组列表的Map<Int,List<Int>>

摘要

因此,本章是关于 Kotlin 中的集合和数据操作。我们通过探索 Kotlin 的集合框架和数据结构开始本章,然后逐渐转向学习 Kotlin 集合框架提供的开箱即用的数据操作和函数。

在下一章中,我们将学习如何一起处理函数式编程、响应式编程和面向对象编程。我们相信 Kotlin 是做这件事的最佳语言,因为它让您能够同时获得函数式编程和面向对象编程的好处。在下一章中,我们将看到如何利用这一点。

在下一章中,我们还将介绍 ReactiveX 框架,这是功能响应式编程中最受欢迎的框架之一。

因此,让我们继续前进;下一章就在眼前。

第九章:函数式编程和响应式编程

到目前为止,我们在过去的八章中取得了良好的进展。你已经学习了函数式编程FP)的概念,以及一些令人惊叹的 Kotlin 特性,如协程和委托,它们并不完全来自 FP 理论(实际上,委托来自 OOP 范式),但所有这些都能使我们从 FP 中获得更多好处。

这篇简短的章节致力于将其他编程原则/范式与 FP 结合,以从它们中获得最佳输出。以下是本章我们将涵盖的主题列表:

  • 将 FP 与 OOP 结合

  • 函数式响应式编程

  • RxKotlin 简介

那么,让我们开始吧。

将 FP 与 OOP 结合

FP 和 OOP 都是老牌的编程范式,各有其优势和劣势。例如,在 FP 中严格遵循无副作用和所有纯函数是很困难的,尤其是对于 FP 初学者和面对复杂项目需求时。然而,在 OOP 系统中,很难避免副作用;此外,OOP 系统通常被称为并发程序的噩梦。

FP 不承认状态,而在现实生活中,状态是无法避免的。

通过使用/结合 OOP 和 FP,可以避免所有这些麻烦。最普遍的混合 OOP 和 FP 的风格可以概括为:在细节上函数式,在整体上面向对象。这是将 OOP 与 FP 结合的简单而高效的方法。这个概念讨论的是在代码中以更高的层次使用 OOP,即在模块化架构中,你可以使用 OOP 来定义类和接口,而在较低层次,即在编写方法/函数时,使用 FP。

为了打破这个概念,考虑一个结构,其中你像使用 OOP 一样编写类和接口,然后在编写函数/方法时,遵循 FP 风格的纯函数、单子和不变性。

如本书前面所述,我们相信如果你想要 OOP 和 FP 的混合,Kotlin 是最好的语言。

函数式响应式编程

函数式响应式编程的概念是通过将 FP 范式与响应式编程相结合而出现的。

函数式响应式编程的定义表明,它是一种用于响应式编程(异步数据流编程)的编程范式,使用 FP 的构建块(例如,mapreducefilter)。

因此,让我们从定义响应式编程开始,然后我们将讨论将它们与 FP 结合。

响应式编程是一种现代编程范式,它讨论的是变化的传播,也就是说,不是将世界表示为一系列状态,而是响应式编程模型行为。

响应式编程是一种异步编程范式,它围绕数据流和变化的传播。简单来说,那些将影响其数据/数据流的所有变化传播给所有相关方(如最终用户、组件和子部分,以及其他以某种方式相关的程序)的程序被称为响应式程序

响应式编程最好通过以下章节中描述的《响应式宣言》来定义。

《响应式宣言》

响应式宣言》(www.reactivemanifesto.org)是一份文件,定义了四个响应式原则,如下所述:

  • 响应性:系统及时响应。响应性系统侧重于提供快速和一致的反应时间,因此它们提供一致的服务质量。

  • 弹性:如果系统遇到任何故障,它仍然保持响应。通过复制、遏制、隔离和委派来实现弹性。故障被包含在每个组件内部,使组件彼此隔离,因此当某个组件发生故障时,它不会影响其他组件或整个系统。

  • 弹性:响应式系统可以响应变化,并在不同的工作负载下保持响应。响应式系统以成本效益的方式在通用硬件和软件平台上实现弹性。

  • 消息驱动:为了建立弹性原则,响应式系统需要通过依赖异步消息传递来在组件之间建立边界。

通过实现前面提到的四个原则,系统变得可靠且响应迅速,因此是响应式的。

Kotlin 的功能响应式框架

为了编写响应式程序,我们需要一个库。目前有几种针对 Kotlin 的响应式编程库可以帮助我们。以下是可用库的列表:

  • RxKotlin

  • Reactor-Kotlin

  • Redux-Kotlin

  • RxKotlin/RxJava 和其他响应式 Java(ReactiveX)框架也可以与 Kotlin 一起使用(因为 Kotlin 与 Java 双向 100%兼容)

在这本书中,我们将重点关注 RxKotlin。

开始使用 RxKotlin

RxKotlin是针对 Kotlin 的响应式编程的具体实现,它受到 FP 的影响。它倾向于函数组合、避免全局状态和副作用。它依赖于生产者/消费者观察者模式,并有许多操作符允许组合、调度、节流、转换、错误处理和生命周期管理。ReactiveX 框架得到了一个大型社区和 Netflix 的支持。

Reactor-Kotlin 也是基于 FP 的;它被广泛接受,并得到 Spring 框架的支持。RxKotlin 和 Reactor-Kotlin 有很多相似之处(可能是因为Reactive Streams规范)。

下载和设置 RxKotlin

您可以从 GitHub 下载并构建 RxKotlin(github.com/ReactiveX/RxKotlin)。它不需要任何其他依赖项。GitHub 维基百科页面上的文档结构良好。以下是您如何从 GitHub 检出项目并运行构建的步骤:

    $ git clone https://github.com/ReactiveX/RxKotlin.git
    $ cd RxKotlin/
    $ ./gradlew build

您也可以使用 Maven 和 Gradle,如页面上的说明。

我们在这本书中使用 RxKotlin 版本 2.2.0。

现在,让我们看看 RxKotlin 究竟是什么。我们将从一个众所周知的事物开始,然后逐渐深入到库的秘密。

将拉取机制与 RxJava 推送机制进行比较

RxKotlin 围绕表示现实生活事件和数据系统的Observable类型,旨在用于推送机制,因此它是懒加载的,可以以同步和异步两种方式使用。

如果我们从与数据列表一起工作的简单示例开始,这将更容易理解:

fun main(args: Array<String>) { 
    var list:List<Any> = listOf(1, "Two", 3, "Four", "Five", 5.5f) // 1 
    var iterator = list.iterator() // 2 
    while (iterator.hasNext()) { // 3 
        println(iterator.next()) // Prints each element 4 
    } 
} 

输出如下:

让我们逐行分析程序,了解它是如何工作的。

在注释1中,我们创建了一个包含七个项目的listlist通过Any类帮助包含混合数据类型的数据)。在注释2中,我们从list创建了一个iterator,这样我们就可以遍历数据。在注释3中,我们创建了一个while循环,使用iteratorlist中拉取数据,然后在注释4中打印它。

这里需要注意的事情是,我们在当前线程阻塞直到收到并准备好数据时从list中拉取数据。例如,想象一下从网络调用/数据库查询而不是从列表中获取数据,在这种情况下,线程将被阻塞多长时间。显然,您可以为此操作创建一个单独的线程,但这也会增加复杂性。

仔细思考一下,哪种方法更好,让程序等待数据,还是当数据可用时将数据推送到程序?

ReactiveX 框架(无论是 RxKotlin 还是 RxJava)的构建块是可观察的。Observable类与迭代器相反。它有一个底层集合或计算,产生可以被消费者消费的值。但不同之处在于,消费者不像在迭代器模式中那样从生产者那里拉取这些值;相反,生产者将值作为通知推送到消费者。

因此,让我们再次使用相同的示例,这次使用observable

fun main(args: Array<String>) { 
    var list = listOf(1, "Two", 3, "Four", "Five", 5.5f) // 1 
    var observable = list.toObservable(); 

    observable.subscribeBy(  // named arguments for lambda Subscribers 
            onNext = { println(it) }, 
            onError =  { it.printStackTrace() }, 
            onComplete = { println("Done!") } 
    ) 
} 

这个程序的结果与上一个相同;它打印出列表中的所有项目。不同之处在于其方法。因此,让我们看看它实际上是如何工作的:

  1. 创建了一个列表(与上一个相同)

  2. 通过list创建了一个Observable实例

  3. 我们订阅了观察者(我们使用 lambda 的命名参数;我们将在后面详细讨论它们)

当我们订阅observable变量时,每个数据都会在准备好后推送到onNext;当所有数据都推送后,它会调用onComplete,如果发生任何错误,则会调用onError

因此,你学习了如何使用Observable实例,并且它们与我们所熟悉的迭代器实例相当相似。我们可以使用这些Observable实例来构建异步流并将数据更新推送到它们的订阅者(甚至是多个订阅者)。这是一个简单的响应式编程范式实现。数据正在传播到所有感兴趣的各方——订阅者。

可观察对象

如我们之前讨论的,在响应式编程中,Observable有一个底层计算,产生可以被消费者(Observer)消费的值。这里最重要的东西是,消费者(Observer)在这里不是拉取值;而是Observable将值推送到消费者。因此,我们可以说Observable接口是一个基于推送、可组合的迭代器,通过一系列操作符将项目发射到最后一个Observer,最终消费这些项目。现在让我们按顺序分解这些内容,以更好地理解:

  • Observer订阅Observable

  • Observable开始发出它所包含的项目

  • ObserverObservable发出的任何项目做出反应

那么,让我们深入了解Observable是如何通过其事件/方法工作的,即onNextonCompleteonError

Observable的工作原理

如我们之前所述,一个Observable值有以下三个最重要的事件/方法:

  • onNextObservable接口将所有项目逐个传递给此方法

  • onComplete:当所有项目都通过onNext方法后,Observable调用onComplete方法

  • onError:当Observable遇到任何错误时,它调用onError方法来处理错误,如果已定义

这里需要注意的一点是,我们所说的Observable中的项目可以是任何东西;它定义为Observable<T>,其中T可以是任何类。甚至可以将数组/列表分配为Observable

让我们看看下面的图示:

下面是一个代码示例,以更好地理解它:

fun main(args: Array<String>) { 

    val observer = object :Observer<Any>{//1 
    override fun onComplete() {//2 
        println("All Completed") 
    } 

        override fun onNext(item: Any) {//3 
            println("Next $item") 
        } 

        override fun onError(e: Throwable) {//4 
            println("Error Occured $e") 
        } 

        override fun onSubscribe(d: Disposable) {//5 
            println("Subscribed to $d") 
        } 
    } 

    val observable = listOf(1, "Two", 3, "Four", "Five", 5.5f).toObservable() //6 

    observable.subscribe(observer)//7 

    val observableOnList = Observable.just(listOf("One", 2, "Three", "Four", 4.5, "Five", 6.0f), 
            listOf("List with 1 Item"), 
            listOf(1,2,3))//8 

    observableOnList.subscribe(observer)//9 
} 

在前面的示例中,我们在注释1处声明了Any数据类型的观察者实例。

在这里,我们利用了Any数据类型的好处。在 Kotlin 中,每个类都是Any类的子类。此外,在 Kotlin 中,一切都是类和对象;没有单独的原始数据类型。

Observer 接口中有四个声明的方法。注释 2 中的 onComplete() 方法在 Observable 完成所有项目且没有任何错误时被调用。在注释 3 中,我们定义了 onNext(item: Any) 函数,这个函数将由 observable 值对每个要发射的项目调用。在这个方法中,我们将数据打印到控制台。在注释 4 中,我们定义了 onError(e: Throwable) 方法,当 Observable 接口遇到错误时将被调用。在注释 5 中,onSubscribe(d: Disposable) 方法将在 Observer 订阅到 Observable 时被调用。在注释 6 中,我们从一个列表中创建了一个 Observable (val observable) 并使用注释 7 中的 observer 值订阅了 observable 值。在注释 8 中,我们再次创建 observable (val observableOnList),它包含列表作为项目。

程序的输出如下:

因此,正如你在输出中看到的,对于第一次订阅(注释 7),当我们订阅到 observable 值时,它调用 onSubscribe 方法,然后 Observable 属性开始发射项目,因为 observer 开始在 onNext 方法中接收它们并将它们打印出来。当 Observable 属性的所有项目都被发射出来后,它调用 onComplete 方法来表示所有项目都已成功发射。第二个也是同样的,只是这里的每个项目都是一个列表。

随着我们逐渐掌握了 Observables,你现在可以学习一些创建 Observable 工厂方法的方法。

Observable.create 方法

在任何时候,你都可以使用 Observable.create 方法创建自己的自定义 Observable 实现。这个方法接受一个 ObservableEmitter<T> 接口的实例作为观察的来源。看看下面的代码示例:

fun main(args: Array<String>) { 

    val observer: Observer<String> = object : Observer<String> { 
        override fun onComplete() { 
            println("All Completed") 
        } 

        override fun onNext(item: String) { 
            println("Next $item") 
        } 

        override fun onError(e: Throwable) { 
            println("Error Occured => ${e.message}") 
        } 

        override fun onSubscribe(d: Disposable) { 
            println("New Subscription ") 
        } 
    }//Create Observer 

    val observable:Observable<String> = Observable.create<String> {//1 
        it.onNext("Emitted 1") 
        it.onNext("Emitted 2") 
        it.onNext("Emitted 3") 
        it.onNext("Emitted 4") 
        it.onComplete() 
    } 

    observable.subscribe(observer) 

    val observable2:Observable<String> = Observable.create<String> {//2 
        it.onNext("Emitted 1") 
        it.onNext("Emitted 2") 
        it.onError(Exception("My Exception")) 
    } 

    observable2.subscribe(observer) 
} 

首先,我们创建了一个 Observer 接口的实例,就像之前的例子一样。我不会详细解释 observer 值,因为我们已经在之前的例子中看到了概述,并且将在本章后面详细讨论。在注释 1 中,我们使用 Observable.create 方法创建了一个 Observable 值。我们通过 onNext 方法从这个 Observable 值中发射了四个字符串,然后使用 onComplete 方法通知它完成。在注释 2 中,我们几乎做了同样的事情,只是在这里,我们不是调用 onComplete,而是调用带有自定义 Exception 函数的 onError

下面是程序的输出:

Observable.create 方法很有用,尤其是在你使用自定义数据结构并且想要控制哪些值被发射出来时。你还可以从不同的线程向观察者发射值。

Observable 合约 (reactivex.io/documentation/contract.html) 指出,Observables 必须按顺序(而不是并行)向观察者发出通知。它们可以从不同的线程发出这些通知,但通知之间必须存在正式的“发生之前”关系。

The Observable.from methods

Observable.from 方法相对于 Observable.create 方法来说比较简单。您可以使用 from 方法从几乎任何 Kotlin 结构中创建 Observable 实例。

在 RxKotlin 1 中,你将拥有 Observale.from 作为一种方法;然而,从 RxKotlin 2.0(与 RxJava2.0 相同),操作符重载已被重命名为后缀,例如 fromArrayfromIterablefromFuture

让我们看看以下代码:

fun main(args: Array<String>) { 

    val observer: Observer<String> = object : Observer<String> { 
        override fun onComplete() { 
            println("Completed") 
        } 

        override fun onNext(item: String) { 
            println("Received-> $item") 
        } 

        override fun onError(e: Throwable) { 
            println("Error Occured => ${e.message}") 
        } 

        override fun onSubscribe(d: Disposable) { 
            println("Subscription") 
        } 
    }//Create Observer 

    val list = listOf("Str 1","Str 2","Str 3","Str 4") 
    val observableFromIterable: Observable<String> = Observable.fromIterable(list)//1 
    observableFromIterable.subscribe(observer) 

    val callable = object : Callable<String> { 
        override fun call(): String { 
            return "I'm From Callable" 
        } 

    } 
    val observableFromCallable:Observable<String> = Observable.fromCallable(callable)//2 
    observableFromCallable.subscribe(observer) 

    val future:Future<String> = object : Future<String> { 
        val retStr = "I'm from Future" 

        override fun get() = retStr 

        override fun get(timeout: Long, unit: TimeUnit?)  = retStr 

        override fun isDone(): Boolean = true 

        override fun isCancelled(): Boolean = false 

        override fun cancel(mayInterruptIfRunning: Boolean): Boolean = false 

    } 
    val observableFromFuture:Observable<String> = Observable.fromFuture(future)//3 
    observableFromFuture.subscribe(observer) 
} 

在注释 1 中,我们使用了 Observable.fromIterable 方法从 Iterable 实例(此处为 list)创建 Observable。在注释 2 中,我们调用了 Observable.fromCallable 方法从 Callable 实例创建 Observable,在注释 3 中,我们调用了 Observable.fromFuture 方法从 Future 实例派生出 Observable

这里是输出:

Iterator.toObservable

多亏了 Kotlin 的扩展函数,您可以将任何 Iterable 实例(如 list)轻松地转换为 Observable。我们已经在 第一章,Kotlin – 数据类型、对象和类 中使用了此方法,但再次看看这个:

fun main(args: Array<String>) { 
    val observer: Observer<String> = object : Observer<String> { 
        override fun onComplete() { 
            println("Completed") 
        } 

        override fun onNext(item: String) { 
            println("Received-> $item") 
        } 

        override fun onError(e: Throwable) { 
            println("Error Occured => ${e.message}") 
        } 

        override fun onSubscribe(d: Disposable) { 
            println("Subscription") 
        } 
    }//Create Observer 
    val list:List<String> = listOf("Str 1","Str 2","Str 3","Str 4") 

    val observable: Observable<String> = list.toObservable() 

    observable.subscribe(observer) 

} 

输出如下:

因此,您难道不好奇想看看 toObservable 方法吗?让我们来做这件事。您可以在 RxKotlin 包提供的 observable.kt 文件中找到此方法:

fun <T : Any> Iterator<T>.toObservable(): Observable<T> = toIterable().toObservable() 
fun <T : Any> Iterable<T>.toObservable(): Observable<T> = Observable.fromIterable(this) 
fun <T : Any> Sequence<T>.toObservable(): Observable<T> = asIterable().toObservable() 

fun <T : Any> Iterable<Observable<out T>>.merge(): Observable<T> = Observable.merge(this.toObservable()) 
fun <T : Any> Iterable<Observable<out T>>.mergeDelayError(): Observable<T> = Observable.mergeDelayError(this.toObservable()) 

因此,它内部使用 Observable.from 方法,这要归功于 Kotlin 的扩展函数。

Subscriber – the Observer interface

在 RxKotlin 1.x 中,Subscriber 操作符在 RxKotlin 2.x 中本质上变成了 Observer 类型。在 RxKotlin 1.x 中有一个 Observer 类型,但 Subscriber 值是你传递给 subscribe() 方法的,并且实现了 Observer。在 RxJava 2.x 中,Subscriber 操作符仅在谈论 Flowables 时存在。

如您在本章前面的示例中所见,Observer 类型是一个接口,其中包含四个方法,即 onNext(item:T)onError(error:Throwable)onComplete()onSubscribe(d:Disposable)。如前所述,当我们连接 ObservableObserver 时,它会在 Observer 类型中寻找这四个方法并调用它们。以下是以下四个方法的简要描述:

  • onNextObservable 调用此方法将每个项逐个传递给 Observer

  • onComplete:当 Observable 想表示它已经完成了向 onNext 方法传递项的操作时,它调用 ObserveronComplete 方法

  • onError:当Observable遇到任何错误时,它会调用onError方法来处理错误,如果Observer类型中定义了错误处理,否则它将抛出异常

  • onSubscribe:每当一个新的Observable订阅到Observer时,都会调用此方法

订阅和处置

所以,我们有Observable(应该观察的事物)和Observer类型(应该观察的类型),现在怎么办?我们如何将它们连接起来?ObservableObserver就像一个输入设备(无论是键盘还是鼠标)和计算机;我们需要某种东西来连接它们(即使是无线输入设备也有一些连接通道,无论是蓝牙还是 Wi-Fi)。

subscribe操作符的作用是连接Observable接口和Observer,充当媒体的作用。我们可以向subscribe操作符传递一到三个方法(onNextonCompleteonError),或者我们可以向subscribe操作符传递Observer接口的实例,以获取与Observer连接的Observable接口。

那么,现在让我们来看一个例子:

fun main(args: Array<String>) { 
    val observable = Observable.range(1,5)//1 

    observable.subscribe({//2 
        //onNext method 
        println("Next-> $it") 
    },{ 
        //onError Method 
        println("Error=> ${it.message}") 
    },{ 
        //onComplete Method 
        println("Done") 
    }) 

    val observer: Observer<Int> = object : Observer<Int> {//3 
    override fun onComplete() { 
        println("All Completed") 
    } 

        override fun onNext(item: Int) { 
            println("Next-> $item") 
        } 

        override fun onError(e: Throwable) { 
            println("Error Occurred=> ${e.message}") 
        } 

        override fun onSubscribe(d: Disposable) { 
            println("New Subscription ") 
        } 
    } 

    observable.subscribe(observer) 
} 

在这个例子中,我们创建了一个Observable实例(在注释1处),并使用不同的重载subscribe操作符使用了两次。在注释2中,我们将三个方法作为参数传递给了subscribe方法。第一个参数是onNext方法,第二个是onError方法,最后一个是onComplete。在注释2中,我们传递了Observer接口的实例。

输出很容易预测,所以我们跳过它。

所以,我们已经了解了订阅的概念,现在可以这样做。如果你想在订阅一段时间后停止发射,怎么办?肯定有办法,对吧?那么,让我们检查一下。

记得ObserveronSubscribe方法吗?在那个方法中有一个我们还没有讨论过的参数。当你订阅时,如果你传递方法而不是Observer实例,那么subscribe操作符将返回一个Disposable实例,或者如果你使用Observer实例,那么你将在onSubscribe方法的参数中得到Disposable实例。

你可以使用Disposable接口的实例在任何给定时间停止发射。让我们来看一个例子:

fun main(args: Array<String>) { 

    val observale = Observable.interval(100, TimeUnit.MILLISECONDS)//1 
    val observer = object : Observer<Long> { 

        lateinit var disposable: Disposable//2 

        override fun onSubscribe(d: Disposable) { 
            disposable = d//3 
        } 

        override fun onNext(item: Long) { 
            println("Received $item") 
            if (item >= 10 && !disposable.isDisposed) {//4 
                disposable.dispose()//5 
                println("Disposed") 
            } 
        } 

        override fun onError(e: Throwable) { 
            println("Error ${e.message}") 
        } 

        override fun onComplete() { 
            println("Complete") 
        } 

    } 
    runBlocking { 
        observale.subscribe(observer) 
        delay(1500)//6 
    } 
} 

这里,我们使用了Observable.interval工厂方法。此方法接受两个参数,描述间隔期间和时间单位;然后按顺序发射从零开始的整数。使用interval创建的Observable永远不会完成,也永远不会停止,除非你取消它们,或者程序停止执行。我认为它非常适合这个场景,因为我们想在中间停止Observable

因此,在这个例子中,在注释1处,我们使用Observable.interval工厂方法创建了Observable,该方法将在每个100毫秒间隔后发射一个整数。

在注释2中,我声明了lateinit var disposableDisposable类型(lateinit意味着变量将在稍后的时间点初始化)。在注释3中,在onSubscribe方法内部,我们将接收到的参数值分配给disposable变量。

我们打算在序列达到10后停止执行,也就是说,在10被发出后,应立即停止发射。为了实现这一点,我们在onNext方法内部放置了一个检查,在那里我们检查发出的项的值。我们检查它是否等于或大于10,如果发射尚未停止(未处置),那么我们就处置发射(注释5)。

这里是输出结果:

从输出中,我们可以看到在调用disposable.dispose()方法后,没有整数被发出,尽管执行等待了 500 毫秒更多(100*10 = 1000 毫秒来打印序列直到10,我们使用1500调用延迟方法,因此是在发出10后的 500 毫秒)。

如果你好奇想了解Disposable接口,那么以下是其定义:

interface Disposable { 
  /** 
 * Dispose the resource, the operation should be idempotent. 
 */ 
  fun dispose() 
  /** 
 * Returns true if this resource has been disposed. 
 * @return true if this resource has been disposed 
 */ 
  val isDisposed:Boolean 
} 

它有一个属性表示发射已经被通知停止(已处置),以及一个通知发射停止(处置)的方法。

摘要

在本章中,你学习了如何将 FP 概念与 OOP 和响应式编程相结合。我们甚至讨论了 RxKotlin,并涵盖了 RxKotlin 的设置和基本用法。

下一章将介绍更高级的 FP 概念——单子、函子、应用,以及如何使用 Kotlin 实现它们。单子、函子和应用是一些必须了解的概念,通常被称为 FP 的构建块。所以,如果你真正想学习 FP,不要跳过下一章。现在就翻到下一页。

第十章:函子、应用和函子

函子、应用和函子是关于函数式编程中最常搜索的词汇之一,如果你考虑到没有人知道它们的意思(实际上,有一些聪明的人知道他们在说什么),这就有意义了。特别是关于函子的混淆已经成为编程社区中的一个笑话/模因:

“一个函子是端内函子的范畴中的幺半群,有什么问题?”

这句话是虚构地归因于詹姆斯·艾里(James Iry)在他的经典博客文章《编程语言的简短、不完整且大部分错误的历史》中引用的菲利普·瓦德勒(Philip Wadler),(james-iry.blogspot.co.uk/2009/05/brief-incomplete-and-mostly-wrong.html)。

在本章中,我们将涵盖以下主题:

  • 函子

  • 选项、列表和作为函子的函数

  • 摩纳哥(Monads)

  • 应用(Applicatives)

函子(Functors)

如果我告诉你,你已经在 Kotlin 中使用了函子,你会感到惊讶吗?让我们看看以下代码:

fun main(args: Array<String>) {
    listOf(1, 2, 3)
            .map { i -> i * 2 }
            .map(Int::toString)
            .forEach(::println)
}

List<T> 类有一个函数,map(transform: (T) -> R): List<R>map 这个名字是从哪里来的?它来自范畴论。当我们从 Int 转换到 String 时,我们是从 Int 范畴映射到 String 范畴。在同样的意义上,在我们的例子中,我们是从 List<Int> 转换到 List<Int>(不是很令人兴奋),然后从 List<Int> 转换到 List<String>。我们没有改变外部类型,只是内部值。

那就是一个函子。一个 函子 是一个定义了如何转换或映射其内容的类型。你可以找到关于函子的不同定义,或多或少是学术性的;但原则上,它们都指向同一个方向。

让我们为函子类型定义一个泛型接口:

interface Functor<C<_>> { //Invalid Kotlin code
    fun <A,B> map(ca: C<A>, transform: (A) -> B): C<B>
}

此外,它无法编译,因为 Kotlin 不支持高阶类型。

你可以在第十三章 Chapter 13 中找到关于 Kotlin 高阶类型(包括替代方案和 Kotlin 的未来)的更多信息,箭头类型**。

在支持高阶类型的语言中,例如 ScalaHaskell,可以定义一个 Functor 类型,例如 Scala cats 函子:

trait Functor[F[_]] extends Invariant[F] { self =>
  def mapA, B(f: A => B): F[B]

  //More code here

在 Kotlin 中,我们没有这些功能,但我们可以通过约定来模拟它们。如果一个类型有一个函数或扩展函数,那么 map 就是一个函子(这被称为结构化类型,通过其结构而不是其层次来定义类型)。

我们可以有一个简单的 Option 类型:

sealed class Option<out T> {
    object None : Option<Nothing>() {
        override fun toString() = "None"
    }

    data class Some<out T>(val value: T) : Option<T>()

    companion object
}

然后,你可以为它定义一个 map 函数:

fun <T, R> Option<T>.map(transform: (T) -> R): Option<R> = when (this) {
    Option.None -> Option.None
    is Option.Some -> Option.Some(transform(value))
}

然后按照以下方式使用它:

fun main(args: Array<String>) {
    println(Option.Some("Kotlin")
            .map(String::toUpperCase)) //Some(value=KOTLIN)
}

现在,一个 Option 值对于 SomeNone 将有不同的行为:

fun main(args: Array<String>) {
    println(Option.Some("Kotlin").map(String::toUpperCase)) //Some(value=KOTLIN)
    println(Option.None.map(String::toUpperCase)) //None
}

扩展函数非常灵活,以至于我们可以为函数类型编写一个 map 函数,即 (A) -> B,因此将函数转换为函子:

fun <A, B, C> ((A) -> B).map(transform: (B) -> C): (A) -> C = { t -> transform(this(t)) }

我们在这里改变的是通过应用参数函数 transform: (B) -> C 到函数 (A) -> B 的结果,将返回类型从 B 改为 C

fun main(args: Array<String>) {
    val add3AndMultiplyBy2: (Int) -> Int = { i: Int -> i + 3 }.map { j -> j * 2 }
    println(add3AndMultiplyBy2(0)) //6
    println(add3AndMultiplyBy2(1)) //8
    println(add3AndMultiplyBy2(2)) //10
}

如果你在其他函数式编程语言中有所经验,你会认出这种行为是前向函数组合(更多关于函数组合的内容请参阅 第十二章,*使用箭头入门)。

Monads

monad 是一个定义了 flatMap(或在其他语言中的 bind)函数的 funtor 类型,该函数接收一个返回相同类型的 lambda。让我用一个例子来解释它。幸运的是,List<T> 定义了一个 flatMap 函数:

fun main(args: Array<String>) {
    val result = listOf(1, 2, 3)
            .flatMap { i ->
                listOf(i * 2, i + 3)
            }
            .joinToString()

    println(result) //2, 4, 4, 5, 6, 6
}

map 函数中,我们只是转换 List 值的内容,但在 flatMap 中,我们可以返回一个具有更少或更多项的新 List 类型,这使得它比 map 更强大得多。

因此,一个通用的 monad 将看起来像这样(记住我们没有高阶类型):

interface Monad<C<_>>: Functor<C> { //Invalid Kotlin code
    fun <A, B> flatMap(ca:C<A>, fm:(A) -> C<B>): C<B>
}

现在,我们可以为我们的 Option 类型编写一个 flatMap 函数:

fun <T, R> Option<T>.flatMap(fm: (T) -> Option<R>): Option<R> = when (this) {
    Option.None -> Option.None
    is Option.Some -> fm(value)
}

如果你仔细观察,你会发现 flatMapmap 非常相似;相似到我们可以用 flatMap 重写 map

fun <T, R> Option<T>.map(transform: (T) -> R): Option<R> = flatMap { t -> Option.Some(transform(t)) }

现在我们可以用 flatMap 函数的强大功能以一些酷炫的方式使用它,这些方式在普通的 map 中是不可能的:

fun calculateDiscount(price: Option<Double>): Option<Double> {
    return price.flatMap { p ->
        if (p > 50.0) {
            Option.Some(5.0)
        } else {
            Option.None
        }
    }
}

fun main(args: Array<String>) {
    println(calculateDiscount(Option.Some(80.0))) //Some(value=5.0)
    println(calculateDiscount(Option.Some(30.0))) //None
    println(calculateDiscount(Option.None)) //None
}

我们的功能函数 calculateDiscount 接收并返回 Option<Double>。如果价格高于 50.0,我们返回一个包裹在 Some 中的 5.0 折扣,如果没有,则返回 None

flatMap 的一个酷炫技巧是它可以嵌套:

fun main(args: Array<String>) {
    val maybeFive = Option.Some(5)
    val maybeTwo = Option.Some(2)

    println(maybeFive.flatMap { f ->
        maybeTwo.flatMap { t ->
            Option.Some(f + t)
        }
    }) // Some(value=7)
}

在内部的 flatMap 函数中,我们可以访问这两个值并对它们进行操作。

我们可以通过组合 flatMapmap 来稍微缩短这个示例的写法:

fun main(args: Array<String>) {
    val maybeFive = Option.Some(5)
    val maybeTwo = Option.Some(2)

    println(maybeFive.flatMap { f ->
        maybeTwo.map { t ->
            f + t
        }
    }) // Some(value=7)
}

因此,我们可以将我们的第一个 flatMap 示例重写为两个列表的组合——一个包含数字,另一个包含函数:

fun main(args: Array<String>) {
    val numbers = listOf(1, 2, 3)
    val functions = listOf<(Int) -> Int>({ i -> i * 2 }, { i -> i + 3 })
    val result = numbers.flatMap { number ->
        functions.map { f -> f(number) }
    }.joinToString()

    println(result) //2, 4, 4, 5, 6, 6
}

这种嵌套多个 flatMapflatMapmap 组合的技术非常强大,并且是另一个名为 monadic comprehensions(第十三章,箭头类型)的概念背后的主要思想,它允许我们组合 monadic 操作(更多关于 comprehensions 的内容请参阅)。

Applicatives

我们之前的例子,在同一个类型的包装器内部调用 lambda,并在同一个包装器内部传递参数,是介绍 applicatives 的完美方式。

applicative 是一个定义了两个函数的类型,一个 pure(t: T) 函数,它返回包裹在 applicative 类型中的 T 值,以及一个 ap 函数(在其他语言中称为 apply),它接收一个包裹在 applicative 类型中的 lambda。

在上一节中,当我们解释 monads 时,我们让它们直接从 funtor 扩展,但事实上,monad 是从 applicative 扩展的,而 applicative 是从 funtor 扩展的。因此,我们的通用 applicative 的伪代码以及整个层次结构将看起来像这样:

interface Functor<C<_>> { //Invalid Kotlin code
    fun <A,B> map(ca:C<A>, transform:(A) -> B): C<B>
}

interface Applicative<C<_>>: Functor<C> { //Invalid Kotlin code
    fun <A> pure(a:A): C<A>

    fun <A, B> ap(ca:C<A>, fab: C<(A) -> B>): C<B>
}

interface Monad<C<_>>: Applicative<C> { //Invalid Kotlin code
    fun <A, B> flatMap(ca:C<A>, fm:(A) -> C<B>): C<B>
}

简而言之,一个 applicative 是一个更强大的 funtor,而一个 monad 是一个更强大的 applicative。

现在,让我们为 List<T> 写一个 ap 扩展函数:

fun <T, R> List<T>.ap(fab: List<(T) -> R>): List<R> = fab.flatMap { f -> this.map(f) }

我们可以回顾一下上一节中 Monads 部分的最后一个示例:

fun main(args: Array<String>) {
    val numbers = listOf(1, 2, 3)
    val functions = listOf<(Int) -> Int>({ i -> i * 2 }, { i -> i + 3 })
    val result = numbers.flatMap { number ->
        functions.map { f -> f(number) }
    }.joinToString()

    println(result) //2, 4, 4, 5, 6, 6
}

让我们用 ap 函数来重写它:

fun main(args: Array<String>) {
    val numbers = listOf(1, 2, 3)
    val functions = listOf<(Int) -> Int>({ i -> i * 2 }, { i -> i + 3 })
    val result = numbers
            .ap(functions)
            .joinToString()
    println(result) //2, 4, 6, 4, 5, 6
}

更容易阅读,但有一个警告——结果顺序不同。我们需要意识到并选择适合我们特定情况的最佳选项。

我们可以向我们的Option类添加pureap

fun <T> Option.Companion.pure(t: T): Option<T> = Option.Some(t)

Option.pure只是Option.Some构造函数的一个简单别名。

我们的Option.ap函数非常迷人:

//Option
fun <T, R> Option<T>.ap(fab: Option<(T) -> R>): Option<R> = fab.flatMap { f -> map(f) }

//List
fun <T, R> List<T>.ap(fab: List<(T) -> R>): List<R> = fab.flatMap { f -> this.map(f) }

Option.apList.ap有相同的主体,使用flatMapmap的组合,这正是我们组合单子操作的方式。

使用单子,我们使用flatMapmap对两个Option<Int>进行求和:

fun main(args: Array<String>) {
    val maybeFive = Option.Some(5)
    val maybeTwo = Option.Some(2)

    println(maybeFive.flatMap { f ->
        maybeTwo.map { t ->
            f + t
        }
    }) // Some(value=7)
}

现在,使用应用函数:

fun main(args: Array<String>) {
    val maybeFive = Option.pure(5)
    val maybeTwo = Option.pure(2)

    println(maybeTwo.ap(maybeFive.map { f -> { t: Int -> f + t } })) // Some(value=7)
}

这不是很容易阅读。首先,我们使用一个 lambda (Int) -> (Int) -> Int(技术上是一个柯里化函数,关于柯里化函数的更多信息可以在第十二章,开始使用 Arrow)来映射maybeFive,它返回一个Option<(Int) -> Int>,可以作为maybeTwo.ap的参数。

我们可以用一个小技巧(我从 Haskell 那里借来的)使事情更容易阅读:

infix fun <T, R> Option<(T) -> R>.`(*)`(o: Option<T>): Option<R> = flatMap { f: (T) -> R -> o.map(f) }

infix扩展函数Option<(T) -> R>.(*) ``将允许我们从左到右读取sum操作;这有多酷?现在,让我们看看以下代码,它是使用应用函数对两个Option`进行求和

fun main(args: Array<String>) {
    val maybeFive = Option.pure(5)
    val maybeTwo = Option.pure(2)

    println(Option.pure { f: Int -> { t: Int -> f + t } } `(*)` maybeFive `(*)` maybeTwo) // Some(value=7)
}

我们用pure函数包装(Int) -> (Int) -> Int lambda,然后逐个应用Option<Int>。我们使用名称(*)作为对 Haskell 的<*>的致敬。

到目前为止,你可以看到应用函数让你可以做一些很酷的技巧,但单子更强大、更灵活。何时使用一个或另一个?这显然取决于你的特定问题,但我们的总体建议是使用尽可能少权力的抽象。你可以从函子map开始,然后是应用函数ap,最后是单子flatMap。所有的事情都可以用flatMap来完成(正如你所见,Optionmapap都是使用flatMap实现的),但大多数时候mapap可以更容易地推理。

回到函数,我们可以使一个函数表现得像一个应用函数。首先,我们应该添加一个纯函数:

object Function1 {
    fun <A, B> pure(b: B) = { _: A -> b }
}

首先,我们创建一个对象Function1,因为函数类型(A) -> B没有伴随对象来添加新的扩展函数,就像我们为Option所做的那样:

fun main(args: Array<String>) {
    val f: (String) -> Int = Function1.pure(0)
    println(f("Hello,"))    //0
    println(f("World"))     //0
    println(f("!"))         //0
}

Function1.pure(t: T)将一个T值包装在一个函数中并返回它,无论我们使用什么参数。如果你有其他函数式语言的经验,你会认出函数的pure作为一个identity函数(关于identity函数的更多信息可以在第十二章,开始使用 Arrow)。

让我们在一个函数(A) -> B中添加flatMapap

fun <A, B, C> ((A) -> B).map(transform: (B) -> C): (A) -> C = { t -> transform(this(t)) }

fun <A, B, C> ((A) -> B).flatMap(fm: (B) -> (A) -> C): (A) -> C = { t -> fm(this(t))(t) }

fun <A, B, C> ((A) -> B).ap(fab: (A) -> (B) -> C): (A) -> C = fab.flatMap { f -> map(f) }

我们已经涵盖了map(transform: (B) -> C): (A) -> C,并且我们知道它表现得像一个正向函数组合。如果你仔细观察flatMapap,你会看到参数有点反方向(并且ap被实现为所有其他类型的ap函数)。

但是,我们能用函数的 ap 做些什么呢?让我们看看下面的代码:

fun main(args: Array<String>) {
    val add3AndMultiplyBy2: (Int) -> Int = { i: Int -> i + 3 }.ap { { j: Int -> j * 2 } }
    println(add3AndMultiplyBy2(0)) //6
    println(add3AndMultiplyBy2(1)) //8
    println(add3AndMultiplyBy2(2)) //10
}

嗯,我们可以组合函数,这并不令人兴奋,因为我们已经用 map 做过这样的事情。但是函数的 ap 有一个小技巧。我们可以访问原始参数:

fun main(args: Array<String>) {
    val add3AndMultiplyBy2: (Int) -> Pair<Int, Int> = { i:Int -> i + 3 }.ap { original -> { j:Int -> original to (j * 2) } }
    println(add3AndMultiplyBy2(0)) //(0, 6)
    println(add3AndMultiplyBy2(1)) //(1, 8)
    println(add3AndMultiplyBy2(2)) //(2, 10)
}

在函数组合中访问原始参数在多个场景下很有用,例如调试和审计。

摘要

我们已经介绍了很多名字吓人但背后理念简单的酷概念。函子、应用和单子类型为我们打开了几种抽象和更强大的函数概念的大门,这些内容我们将在接下来的章节中介绍。我们学习了 Kotlin 的一些局限性以及我们如何在创建函数来模拟不同类型的函子、应用和单子时克服它们。我们还探讨了函子、应用和单子之间的层次关系。

在下一章中,我们将介绍如何有效地处理数据流。

第十一章:在 Kotlin 中使用流

因此,我们正在逐步完成这本书。在本章中,我们将介绍 Kotlin 中的流以及如何使用它们。

流首次在 Java 8 中被引入。Kotlin 中的 Stream API 几乎与 Java API 相同,但包含了一些小的补充和扩展函数。

这是本章我们将要涵盖的内容:

  • 流的介绍

  • 集合与流

  • 流与可观察对象(ReactiveX-RxKotlin/RxJava)

  • 流的使用

  • 创建流的多种方式

  • 流的收集

那么,让我们开始吧。

流的介绍

如我们之前提到的,流(Streams)首次在 Java 8 中被引入。从 Java 8 开始,Java 开始更加关注函数式编程,并逐渐添加了函数式特性。

相反,Kotlin 从第一天开始就添加了函数式特性。Kotlin 添加了函数式特性和接口。当使用 Java 时,只有在使用 Java 8 及更高版本时才能使用流,但使用 Kotlin,即使在与 JDK 6 一起工作时,你仍然可以使用流。

那么,什么是流呢?你可以将流想象为一个抽象层,它覆盖了一系列元素以执行聚合操作。困惑吗?让我们通过一个代码示例来尝试理解:

  fun main(args: Array<String>) { 
      val stream = 1.rangeTo(10).asSequence().asStream() 
      val resultantList = stream.skip(5).collect(Collectors.toList()) 
      println(resultantList) 
  } 

输出结果如下:

图片

在之前的程序中,我们所做的是创建一个IntRange值,从它创建一个Sequence值,然后从它获取stream值。然后我们跳过了前五个元素,并将其收集回一个List实例。我们将在本章后面详细查看之前代码中使用的所有函数。

之前的程序使用了 Stream API 的函数式接口。

Stream API 拥有丰富的函数式接口,就像我们在集合中看到的那样。

集合与流

读到这儿,你可能正在想,在那个程序中我们执行的所有操作在 Kotlin 的集合中也是可能的,那么为什么我们要使用流呢?为了回答这个问题,我们首先应该了解流和集合之间的区别。让我们看一下以下列出的集合和流之间的区别列表:

  • 正如集合的定义所说,集合是一个数据结构,它存储并允许你处理一组数据。另一方面,不是数据结构,不存储任何东西;它们像管道或 IO 通道一样工作,按需从其源获取数据。

  • 每个数据结构都必须有一个有限的大小限制,集合也是如此。但是,由于流不是数据结构,它们不需要有特定的尺寸限制。

  • 当直接访问集合的元素时,你可以随时进行,即使是相同的位置,也不需要重新创建集合。但是,当与流(Streams)一起工作时,流中的元素在流的生命周期内只会被访问一次。就像迭代器一样,必须生成一个新的流来重新访问源中的相同元素。

  • 集合 API 以急切的方式构建对象,总是准备好被消费。流 API 以懒加载、按需的方式创建对象。

  • 集合(Collection)API 用于在多种数据结构中存储数据。流(Stream)API 用于在大量对象上计算数据。

因此,这些是集合 API 和流 API 之间非常基本的不同之处。乍一看,流(Streams)似乎像是 RxKotlin 的可观察者(Observables),它们提供了一种消费数据的方式,但流(Streams)和可观察者(Observables)之间有很多显著的差异。这些是流(Streams)和可观察者(Observables)之间的差异:

  • 第一个值得注意的区别是,流(Streams)是基于拉取的,而可观察者(Observables)是基于推送的。这可能听起来过于抽象,但它有非常具体的重大影响。

  • 使用可观察者(Observables),由于调度器(Schedulers)的存在,很容易更改线程或轻松指定链的线程池。但是,使用流(Streams)则有点棘手。

  • 可观察者(Observables)在整个过程中都是同步的。这让你不必总是检查这些基本操作是否线程安全。

  • 另一个显著的区别是,可观察者(Observables)比流(Streams)API 拥有更多的功能接口,这使得可观察者易于使用,并且有更多选项来完成特定任务。

因此,我们了解到了流(Streams)不是数据结构,而是数据源(可能是集合或其他任何东西)上方的抽象层,尽管流(Streams)以懒加载、按需的方式构建对象,但它们仍然是基于拉取的,并在其中使用循环。

要了解更多关于基于推送的架构和可观察者(Observables)的信息,你可以阅读 Rivu Chakraborty 所著的《Kotlin 中的响应式编程》一书。

处理流(Streams)

因此,我们学习了许多关于流(Streams)的理论,我们还了解到流(Streams)有一组功能接口来与之交互(实际上,功能接口是唯一与流(Streams)交互的方式),但正如我之前提到的,它们的工作方式与集合 API 略有不同。

为了更清楚地说明,请回顾以下示例:

  fun main(args: Array<String>) { 
      val stream = 1.rangeTo(10).asSequence().asStream() 
       val resultantList = stream.filter{ 
          it%2==0 
      }.collect(Collectors.toList()) 
      println(resultantList) 
  } 

上述程序是一个简单的例子;我们只是抓取了一个从110的数字流,并从该流中过滤出奇数,然后将结果收集到一个新的List中。

但让我们尝试理解它是如何工作的机制。我们已经熟悉了函数式接口和 filter 函数,因为我们已经在前面的章节中介绍了它们,但这里不同的是 collect 函数和 Collectors 值,它们有助于将结果数据收集到新的 List 中。我们将在本章后面更详细地讨论 collect 方法以及 Collectors 值,但现在,让我们看看 Streams 提供的函数式接口和流类型。

因此,以下是从 Stream API 中提取的操作/函数式接口及其描述:

  • filter(): 与 Kotlin 中的 Collection.filter 功能相同。它返回一个由与此 stream 中匹配给定谓词的元素组成的 stream 值。

  • map(): 与 Kotlin 中的 Collection.map 功能相同。它返回一个由将给定函数应用于此 stream 的每个元素的结果组成的 stream 值。

  • mapToInt()/mapToLong()/mapToDouble(): 与 map 的功能相同,但返回的不是 stream 值,而是分别返回 IntStreamLongStreamDoubleStream 值。我们将在本章后面详细讨论 IntStreamLongStreamDoubleStream

  • flatMap(): 与 Kotlin 中的 Collection.flatMap 功能相同。

  • flatMapToInt()/flatMapToLong()/flatMapToDouble(): 与 flatMap 的功能相同,但返回的不是 stream 值,而是分别返回 IntStreamLongStreamDoubleStream 值。

  • distinct(): 与 Collection.distinct 的功能相同。它返回一个包含不同元素的 stream 值。

  • peek(): 这个函数在 Kotlin 集合中没有对应的函数,然而在 RxKotlin/RxJava 中有对应的函数。这个函数返回由这个 stream 的元素组成的 stream 值,并在每个元素被消费的同时执行提供的操作,就像 RxJava 中的 doOnNext 操作符一样。

  • anyMatch(): 与 Collection.any() 类似,它返回此 stream 的任何元素是否都匹配提供的谓词。如果不需要评估所有元素来确定结果,则可能不会评估谓词。如果 stream 值为空,则返回 false 值,并且不会评估谓词。

  • allMatch(): 与 Collection.all 类似,它返回此 stream 的所有元素是否都匹配提供的谓词。如果不需要评估所有元素来确定结果,则可能不会评估谓词。如果 stream 值为空,则返回 true 值,并且不会评估谓词。

  • noneMatch():类似于Collection.none,它返回此stream中是否有元素匹配提供的谓词。如果不需要确定结果,则可能不会评估谓词。如果stream为空,则返回false值,并且不会评估谓词。

我们省略了这些函数的示例,因为它们与Collection函数和 RxJava/RxKotlin 操作符类似。

如果你对此有所疑问,那么是的,如果你的项目完全是 Kotlin(没有任何 Java 或其他语言代码),你可以安全地放弃 Streams,转而使用 Collections 和协程。

现在,让我们来看看之前提到的IntStreamDoubleStreamLongStream值,并探讨它们的作用。

原始流

原始流是在 Java 8 中引入的,以便在使用 Streams 的同时利用 Java 中的原始数据类型(再次强调,Streams 基本上来自 Java,而 Kotlin 只是为 Streams API 添加了一些扩展函数)。IntStreamLongStreamDoubleStream是这些原始流的一部分。

这些原始流的工作方式与普通 Stream 类似,但增加了一些原始数据类型的功能。

那么,让我们举一个例子;看看以下程序:

  fun main(args: Array<String>) { 
      val intStream = IntStream.range(1,10) 
      val result = intStream.sum() 
      println("The sum of elements is $result") 
  } 

因此,我们使用IntStream.range()函数创建了一个IntStream值,range函数接受两个整数作为起始点和结束点,并创建一个从指定整数开始的 Stream,包括这两个整数。然后我们计算了总和并打印出来。程序看起来相当简单,显然要归功于IntStream,为什么?想想用这种方式轻松计算元素的总和;没有IntStream,我们就必须遍历所有元素来计算总和。

下面是另一个原始流的例子:

  fun main(args: Array<String>) { 
      val doubleStream = DoubleStream.iterate(1.5,{item ->     item*1.3})//(1) 
      val avg = doubleStream 
              .limit(10)//(2) 
              .peek { 
                  println("Item $it") 
              }.average()//(3) 
      println("Average of 10 Items $avg") 
  } 

在我们解释程序之前,先看看以下输出:

那么,让我们来解释一下这个程序:

  • 在注释(1)中,我们使用iterate()工厂方法创建了一个DoubleStream值。iterate方法接受一个double作为 Stream 的种子,以及一个操作符,该操作符将被迭代应用以生成 Stream 的元素,例如,如果你传递x作为种子和f作为操作符,Stream 将返回x作为第一个元素,f(x)作为第二个元素,f(f(x))作为第三个元素,依此类推。此函数创建了一个无限大小的 Stream。

  • 我们在注释(2)处使用了limit操作符,因为我们只想从这个 Stream 中获取 10 个元素,而不是无限多的所有元素。在注释(3)处,我们计算了average

那么,让我们来看看创建 Stream 的不同方法。

Stream 工厂方法

Streams API 提供了多种获取Stream实例的方法。以下是创建 Streams 的几种方式,我们将对其进行介绍:

  • Stream Builder

  • Stream.empty()

  • Stream.of()

  • Stream.generate()

  • Stream.iterate()

  • Kotlin 扩展——asStream()

在前面的列表中,我们已经看到了 Kotlin 扩展asStreamStream.iterate函数的工作方式(它将以与前面示例中覆盖的DoubleStream.iterate值相同的方式工作)。我们将查看其余部分。

Stream Builder

Stream Builder接口使得轻松创建流实例变得非常容易。看看以下示例:

  fun main(args: Array<String>) { 
      val stream = Stream.builder<String>() 
              .add("Item 1") 
              .add("Item 2") 
              .add("Item 3") 
              .add("Item 4") 
              .add("Item 5") 
              .add("Item 6") 
              .add("Item 7") 
              .add("Item 8") 
              .add("Item 9") 
              .add("Item 10") 
              .build() 
      println("The Stream is ${stream.collect(Collectors.toList())}") 
  } 

输出如下:

图片

Stream.builder()方法返回一个Streams.Builder实例。然后,我们使用了Builder.add函数;add函数接受要构建的stream值的项,并返回相同的Stream.Builder实例。然后build函数使用提供给构建器的项创建stream实例。

创建空流 – Stream.empty()

使用Streams.empty()工厂方法创建空流非常简单。考虑以下示例:

  fun main(args: Array<String>) { 
      val emptyStream = Stream.empty<String>() 
      val item = emptyStream.findAny() 
      println("Item is $item") 
 } 

在前面的示例中,我们使用Stream.empty()创建了emptyStream值,然后使用findAny()函数随机从该流中获取任何元素。findAny()方法返回一个包含从流中随机选择的项的Optional值,如果流为空,则返回一个空的Optional

以下是为前一个程序生成的输出:

图片

通过传递元素创建流 – Stream.of()

我们还可以通过将元素提供给of函数来获取流的一个实例。of函数的工作方式与 RxJava/RxKotlin 中的Observable.just方法类似。

看看以下示例:

 fun main(args: Array<String>) { 
      val stream = Stream.of("Item 1",2,"Item 3",4,5.0,"Item 6") 
      println("Items in Stream =            ${stream.collect(Collectors.toList())}") 
  } 

输出如下:

图片

简单直接,不是吗?

生成流 – Stream.generate()

我们还可以通过使用Stream.generate()工厂方法来创建流。它接受一个 lambda/supplier 实例作为参数,并在每次需要项时使用它来生成项。此方法也会创建一个无限流。

考虑以下示例:

  fun main(args: Array<String>) { 
      val stream = Stream.generate { 
          //return a random number 
          (1..20).random() 
      } 
      val resultantList = stream 
              .limit(10) 
              .collect(Collectors.toList()) 
      println("resultantList = $resultantList") 
  } 

输出如下:

图片

因此,Stream API 调用了 lambda 来获取流中的每个元素——太棒了。

因此,现在我们相当熟悉如何使用流以及原始流,让我们继续前进,看看如何使用Collectors

收集器和 Stream.collect – 收集流

我们可以使用流执行许多操作,但我们可能会遇到需要将流中的元素重新包装到数据结构中的情况。Stream.collect()方法帮助我们实现这一点。它是 Streams API 的终端方法之一。它允许你在流实例中持有的数据元素上执行可变的fold操作(将元素重新包装到某些数据结构中并应用一些附加逻辑,连接它们等)。

collect() 方法接受一个 Collector 接口实现作为参数,用于收集策略(是否将它们重新包装到数据结构中、连接它们,或其他任何操作)。

那么,我们是否需要编写自己的 Collector 接口实现来将 Stream 重新包装成 List/Set 值?当然不需要,Streams API 为一些最常见的用例提供了一些预定义的 Collector 实现。

Collectors 类包含预定义的 Collector 实现。所有这些都可以通过以下行导入:

import java.util.stream.Collectors 

以下列表包含预定义的 Collector 实现:

  • Collectors.toList()

  • Collectors.toSet()

  • Collectors.toMap()

  • Collectors.toCollection()

  • Collectors.joining()

  • Collectors.groupingBy()

因此,让我们简要地看一下每个方法。

Collectors.toList()Collectors.toSet()Collectors.toCollection() 方法

我们已经看到了 Collectors.toList() 的实现。Collectors.toList() 方法帮助将 Stream 的元素收集到 List 中。这里需要注意的是,你不能指定要使用哪个 List 实现;相反,它将始终使用默认的实现。

Collectors.toSet()Collectors.toList() 方法类似,只是它将元素重新包装到集合中。同样,使用 Collectors.toSet(),你将无法指定要使用哪个集合实现。

Collectors.toCollection() 方法是 toList()toSet() 的补充版本;它允许你提供一个自定义的 Collection 来累积列表。

考虑以下示例来解释它:

  fun main(args: Array<String>) { 
      val resultantSet = (0..10).asSequence().asStream() 
              .collect(Collectors.toCollection{LinkedHashSet<Int>()}) 
      println("resultantSet $resultantSet") 
  } 

输出如下所示:

图片

收集到 Map – Collectors.toMap()

Collectors.toMap() 函数帮助我们重新包装 Stream 成 Map 实现。此函数提供了许多自定义选项。最简单的版本接受两个 lambda 表达式;第一个用于确定 Map Entry 的键,第二个用于确定 Map Entry 的值。请注意,Stream 中的每个元素都将表示为 Map 中的一个条目。

这两个 lambda 表达式将在单独的迭代中获取 Stream 的每个元素,并基于它们生成键/值。

看一下以下示例:

  fun main(args: Array<String>) { 
      val resultantMap = (0..10).asSequence().asStream() 
              .collect(Collectors.toMap<Int,Int,Int>({ 
                  it 
              },{ 
                  it*it 
              })) 
      println("resultantMap = $resultantMap") 
  } 

在这个程序中,我们使用了 Collectors.toMap() 函数的最简单版本。我们向其传递了两个 lambda 表达式,第一个用于确定条目的键,将返回传递给它的相同值,第二个则相反,计算并返回传递值的平方。这里需要注意的是,这两个 lambda 表达式将具有相同的参数。

输出如下所示:

图片

字符串 Stream 的连接 – Collectors.joining()

Collectors.joining() 函数帮助您将包含字符串的 Stream 的元素连接起来。它有三个可选参数,即——delimiter(分隔符)、prefix(前缀)和postfix(后缀)。

考虑以下程序的示例:

  fun main(args: Array<String>) { 
      val resultantString = Stream.builder<String>() 
              .add("Item 1") 
              .add("Item 2") 
              .add("Item 3") 
              .add("Item 4") 
              .add("Item 5") 
              .add("Item 6") 
              .build() 
              .collect(Collectors.joining(" - ","Starts Here=>","<=Ends   Here")) 

      println("resultantString $resultantString") 
  } 

输出如下:

Stream 元素分组 – Collectors.groupingBy()

此函数允许我们在分组的同时将 Stream 的元素收集到一个 Map 函数中。此函数与 Collectors.toMap 的基本区别在于,此函数允许您创建一个 Map<K,List<T>> 函数,也就是说,它允许您创建一个 Map 函数,该函数将为每个组保留一个 List 值作为其值。

考虑以下示例:

  fun main(args: Array<String>) { 
      val resultantSet = (1..20).asSequence().asStream() 
              .collect(Collectors.groupingBy<Int,Int> { it%5 }) 
      println("resultantSet $resultantSet") 
  } 

输出如下:

摘要

因此,在本章中,我们学习了关于 Streams 的内容。我们学习了如何创建 Streams,学习了如何与 Streams 一起工作,以及如何将 Stream 打包成 Collections。

在下一章中,我们将开始学习 Arrow 库,它使得在 Kotlin 中实现函数式编程变得容易。所以,不要只是等待,翻到下一页,开始学习 Arrow。

第十二章:开始使用 Arrow

Arrow (arrow-kt.io/) 是一个 Kotlin 库,它提供了函数式构造、数据类型和其他抽象。Kotlin 语法强大且灵活,Arrow 利用这一点提供了一些标准中不包含的功能。

Arrow 是将两个最成功和最受欢迎的函数式库 funKTionaleKategory 结合成一个的结果。在 2017 年晚些时候,两个开发者团队担心分裂会损害整个 Kotlin 社区,因此决定联合起来创建一个单一、统一的函数式库。

在本章中,我们将介绍如何使用现有函数构建新的和更丰富的函数。我们将涵盖的一些主题包括以下内容:

  • 函数组合

  • 部分应用

  • 柯里化

  • 缓存

  • 管道

  • 光学

函数组合

函数式编程作为一个概念的一个重要部分是,以我们使用任何其他类型的方式使用函数——作为值、参数、返回值等。我们可以用其他类型做的一件事是将它们作为构建其他类型的构建块;同样的概念也可以应用于函数。

函数组合是一种使用现有函数构建函数的技术;类似于 Unix 管道或通道管道,函数的结果值被用作下一个函数的参数。

在 Arrow 中,函数组合以一系列的 infix 扩展函数的形式出现:

函数 描述
compose 将右手函数调用的结果作为左手函数的参数。
forwardCompose 将左手函数调用的结果作为右手函数的参数。
andThen forwardCompose 的别名。

让我们组合一些函数:

import arrow.syntax.function.andThen
import arrow.syntax.function.compose
import arrow.syntax.function.forwardCompose
import java.util.*

val p: (String) -> String = { body -> "<p>$body</p>" }

val span: (String) -> String = { body -> "<span>$body</span>" }

val div: (String) -> String = { body -> "<div>$body</div>" }

val randomNames: () -> String = {
    if (Random().nextInt() % 2 == 0) {
        "foo"
    } else {
        "bar"
    }
}

fun main(args: Array<String>) {
    val divStrong: (String) -> String = div compose strong

    val spanP: (String) -> String = p forwardCompose span

    val randomStrong: () -> String = randomNames andThen strong

    println(divStrong("Hello composition world!"))
    println(spanP("Hello composition world!"))
    println(randomStrong())
}

要构建 divStrong: (String) -> String 函数,我们组合 div:(String) -> Stringstrong:(String) -> String。换句话说,divStrong 等同于以下代码片段:

val divStrong: (String) -> String = { body -> "<div><strong>$body</div></strong>"}

对于 spanP:(String) -> String,我们按照以下方式组合 span:(String) -> (String)p:(String) -> String

val spanP: (String) -> String = { body -> "<span><p>$body</p></span>"}

注意我们使用的是相同的类型 (String) -> String,但任何具有其他函数所需正确返回类型的函数都可以进行组合。

让我们用函数组合重写我们的 Channel 管道示例:

data class Quote(val value: Double, val client: String, val item: String, val quantity: Int)

data class Bill(val value: Double, val client: String)

data class PickingOrder(val item: String, val quantity: Int)

fun calculatePrice(quote: Quote) = Bill(quote.value * quote.quantity, quote.client) to PickingOrder(quote.item, quote.quantity)

fun filterBills(billAndOrder: Pair<Bill, PickingOrder>): Pair<Bill, PickingOrder>? {
   val (bill, _) = billAndOrder
   return if (bill.value >= 100) {
      billAndOrder
   } else {
      null
   }
}

fun warehouse(order: PickingOrder) {
   println("Processing order = $order")
}

fun accounting(bill: Bill) {
   println("processing = $bill")
}

fun splitter(billAndOrder: Pair<Bill, PickingOrder>?) {
   if (billAndOrder != null) {
      warehouse(billAndOrder.second)
      accounting(billAndOrder.first)
   }
}

fun main(args: Array<String>) {
   val salesSystem:(Quote) -> Unit = ::calculatePrice andThen ::filterBills forwardCompose ::splitter
   salesSystem(Quote(20.0, "Foo", "Shoes", 1))
   salesSystem(Quote(20.0, "Bar", "Shoes", 200))
   salesSystem(Quote(2000.0, "Foo", "Motorbike", 1))
}

salesSystem: (Quote) -> Unit 函数的行为非常复杂,但它是由其他函数作为构建块构建的。

部分应用

使用函数组合,我们通过取两个函数来创建第三个函数;使用部分应用,我们通过向现有函数传递参数来创建一个新的函数。

Arrow 提供了两种部分应用风格——显式和隐式。

显式风格使用一系列称为 partially1partially2,一直到 partially22 的扩展函数。隐式风格通过一系列扩展,重载了 invoke 操作符:

package com.packtpub.functionalkotlin.chapter11

import arrow.syntax.function.invoke
import arrow.syntax.function.partially3

fun main(args: Array<String>) {
   val strong: (String, String, String) -> String = { body, id, style -> "<strong id=\"$id\" style=\"$style\">$body</strong>" }

   val redStrong: (String, String) -> String = strong.partially3("font: red") //Explicit

   val blueStrong: (String, String) -> String = strong(p3 = "font: blue") //Implicit

   println(redStrong("Red Sonja", "movie1"))
   println(blueStrong("Deep Blue Sea", "movie2"))
}

两种风格都可以按以下方式链式使用:

fun partialSplitter(billAndOrder: Pair<Bill, PickingOrder>?, warehouse: (PickingOrder) -> Unit, accounting: (Bill) -> Unit) {
   if (billAndOrder != null) {
      warehouse(billAndOrder.second)
      accounting(billAndOrder.first)
   }
}

fun main(args: Array<String>) {
   val splitter: (billAndOrder: Pair<Bill, PickingOrder>?) -> Unit = ::partialSplitter.partially2 { order -> println("TESTING $order") }(p2 = ::accounting)

   val salesSystem: (quote: Quote) -> Unit = ::calculatePrice andThen ::filterBills forwardCompose splitter
   salesSystem(Quote(20.0, "Foo", "Shoes", 1))
   salesSystem(Quote(20.0, "Bar", "Shoes", 200))
   salesSystem(Quote(2000.0, "Foo", "Motorbike", 1))
}

我们原始的 splitter 函数不够灵活,因为它直接调用了仓库和会计函数。partialSplitter 函数通过将 warehouseaccounting 作为参数来解决这个问题;然而,一个 (Pair<Bill, PickingOrder>?, (PickingOrder) -> Unit, (Bill) -> Unit) 函数不能用于组合。然后,我们部分应用了两个函数——一个 lambda 和一个引用。

绑定

部分应用的一个特殊情况是 绑定。使用绑定时,你向 (T) -> R 函数传递一个 T 参数,但不执行它,实际上返回一个 () -> R 函数:

fun main(args: Array<String>) {

   val footer:(String) -> String = {content -> "<footer&gt;$content</footer>"}
   val fixFooter: () -> String = footer.bind("Functional Kotlin - 2018") //alias for partially1
   println(fixFooter())
}

bind 函数只是 partially1 的别名,但给它一个单独的名字并使其语义更加正确是有意义的。

反转

反转接受任何函数并返回其参数顺序相反的函数(在其他语言中,此函数被称为 flip)。让我们看看以下代码:

import arrow.syntax.function.partially3
import arrow.syntax.function.reverse

fun main(args: Array<String>) {
   val strong: (String, String, String) -> String = { body, id, style -> "<strong id=\"$id\" style=\"$style\">$body</strong>" }

   val redStrong: (String, String) -> String = strong.partially3("font: red") //Explicit

   println(redStrong("Red Sonja", "movie1"))

   println(redStrong.reverse()("movie2", "The Hunt for Red October"))

}

我们的 redStrong 函数使用起来有些笨拙,因为我们期望首先有 id 然后有 body,但可以通过 reverse 扩展函数轻松修复。reverse 函数可以应用于从参数 122 的函数。

管道

pipe 函数接受一个 T 值并使用它调用 (T) -> R 函数:

import arrow.syntax.function.pipe

fun main(args: Array<String>) {
    val strong: (String) -> String = { body -> "<strong>$body</strong>" }

   "From a pipe".pipe(strong).pipe(::println)
}

管道类似于函数组合,但不同之处在于我们不是生成新函数,而是可以链式调用函数以产生新的值,从而减少嵌套调用。管道在其他语言中,如 ElmOcaml 中,被称为操作符 |>

fun main(args: Array<String>) {
   splitter(filterBills(calculatePrice(Quote(20.0, "Foo", "Shoes", 1)))) //Nested

   Quote(20.0, "Foo", "Shoes", 1) pipe ::calculatePrice pipe ::filterBills pipe ::splitter //Pipe
}

两行是等价的,但第一行必须从后往前理解,第二行应该从左到右阅读:

import arrow.syntax.function.pipe
import arrow.syntax.function.pipe3
import arrow.syntax.function.reverse

fun main(args: Array<String>) {
   val strong: (String, String, String) -> String = { body, id, style -> "<strong id=\"$id\" style=\"$style\">$body</strong>" }

   val redStrong: (String, String) -> String = "color: red" pipe3 strong.reverse()

   redStrong("movie3", "Three colors: Red") pipe ::println
}

pipe 应用于多参数函数时,使用其变体 pipe2pipe22,它表现得像 partially1

柯里化

将柯里化应用于具有 n 个参数的函数,例如 (A, B) -> R,将其转换为 n 个函数调用的链,(A) -> (B) -> R

import arrow.syntax.function.curried
import arrow.syntax.function.pipe
import arrow.syntax.function.reverse
import arrow.syntax.function.uncurried

fun main(args: Array<String>) {

   val strong: (String, String, String) -> String = { body, id, style -> "<strong id=\"$id\" style=\"$style\">$body</strong>" }

   val curriedStrong: (style: String) -> (id: String) -> (body: String) -> String = strong.reverse().curried()

   val greenStrong: (id: String) -> (body: String) -> String = curriedStrong("color:green")

   val uncurriedGreenStrong: (id: String, body: String) -> String = greenStrong.uncurried()

   println(greenStrong("movie5")("Green Inferno"))

   println(uncurriedGreenStrong("movie6", "Green Hornet"))

   "Fried Green Tomatoes" pipe ("movie7" pipe greenStrong) pipe ::println
}

柯里化形式的函数可以通过 uncurried() 转换为正常的多参数形式。

柯里化和部分应用之间的区别

在柯里化和部分应用之间存在一些混淆。一些作者将它们视为同义词,但它们是不同的:

import arrow.syntax.function.curried
import arrow.syntax.function.invoke

fun main(args: Array<String>) {
   val strong: (String, String, String) -> String = { body, id, style -> "<strong id=\"$id\" style=\"$style\">$body</strong>" }

   println(strong.curried()("Batman Begins")("trilogy1")("color:black")) // Curried

   println(strong("The Dark Knight")("trilogy2")("color:black")) // Fake curried, just partial application

   println(strong(p2 = "trilogy3")(p2 = "color:black")("The Dark Knight rises")) // partial application   
}

这些区别是显著的,并且可以帮助我们决定何时使用其中一个:

柯里化 部分应用
返回值 当一个 N 元函数被柯里化时,它返回一个大小为 N 的函数链,(柯里化形式)。 当一个函数或 N 元函数被部分应用时,它返回一个 N - 1 元函数。
参数应用 柯里化后,只有链的第一个参数可以被应用。 任何参数都可以以任何顺序应用。
还原 可以将柯里化形式的函数还原为多参数函数。 由于部分应用不会改变函数形式,因此还原不适用。

部分应用可以更加灵活,但一些函数式风格倾向于偏好柯里化。重要的是要掌握的是,这两种风格都是不同的,并且都由 Arrow 支持。

逻辑补码

逻辑补码接受任何谓词(一个返回 Boolean 类型的函数)并将其否定。让我们看看以下代码:

import arrow.core.Predicate
import arrow.syntax.function.complement

fun main(args: Array<String>) {
   val evenPredicate: Predicate<Int> = { i: Int -> i % 2 == 0 }
   val oddPredicate: (Int) -> Boolean = evenPredicate.complement()

   val numbers: IntRange = 1..10
   val evenNumbers: List<Int> = numbers.filter(evenPredicate)
   val oddNumbers: List<Int> = numbers.filter(oddPredicate)

   println(evenNumbers)
   println(oddNumbers)
}

注意,我们使用的是 Predicate<T> 类型,但它只是 (T) -> Boolean 的别名。从 022 参数的谓词都有补充扩展函数。

缓存

缓存是一种缓存纯函数结果的技巧。缓存函数的行为像一个普通函数,但它存储了与产生该结果提供的参数相关联的先前计算的结果。

缓存化的经典例子是斐波那契数列:

import arrow.syntax.function.memoize
import kotlin.system.measureNanoTime

fun recursiveFib(n: Long): Long = if (n < 2) {
   n
} else {
   recursiveFib(n - 1) + recursiveFib(n - 2)
}

fun imperativeFib(n: Long): Long {
   return when (n) {
      0L -> 0
      1L -> 1
      else -> {
         var a = 0L
         var b = 1L
         var c = 0L
         for (i in 2..n) {
            c = a + b
            a = b
            b = c
         }
         c
      }
   }
}

fun main(args: Array<String>) {

   var lambdaFib: (Long) -> Long = { it } //Declared ahead to be used inside recursively

   lambdaFib = { n: Long ->
      if (n < 2) n else lambdaFib(n - 1) + lambdaFib(n - 2)
   }

   var memoizedFib: (Long) -> Long = { it }

   memoizedFib = { n: Long ->
      if (n < 2) n else memoizedFib(n - 1) + memoizedFib(n - 2)
   }.memoize()

   println(milliseconds("imperative fib") { imperativeFib(40) }) //0.006

   println(milliseconds("recursive fib") { recursiveFib(40) }) //1143.167
   println(milliseconds("lambda fib") { lambdaFib(40) }) //4324.890
   println(milliseconds("memoized fib") { memoizedFib(40) }) //1.588
}

inline fun milliseconds(description: String, body: () -> Unit): String {
   return "$description:${measureNanoTime(body) / 1_000_000.00} ms"
}

我们缓存版本的执行速度比递归函数版本快 700 多倍(后者几乎比 lambda 版本快四倍)。命令式版本是无敌的,因为它被编译器高度优化:

fun main(args: Array<String>) = runBlocking {

   var lambdaFib: (Long) -> Long = { it } //Declared ahead to be used inside recursively

   lambdaFib = { n: Long ->
      if (n < 2) n else lambdaFib(n - 1) + lambdaFib(n - 2)
   }

   var memoizedFib: (Long) -> Long = { it }

   memoizedFib = { n: Long ->
      println("from memoized fib n = $n")
      if (n < 2) n else memoizedFib(n - 1) + memoizedFib(n - 2)
   }.memoize()
   val job = launch {
      repeat(10) { i ->
         launch(coroutineContext) { println(milliseconds("On coroutine $i - imperative fib") { imperativeFib(40) }) }
         launch(coroutineContext) { println(milliseconds("On coroutine $i - recursive fib") { recursiveFib(40) }) }
         launch(coroutineContext) { println(milliseconds("On coroutine $i - lambda fib") { lambdaFib(40) }) }
         launch(coroutineContext) { println(milliseconds("On coroutine $i - memoized fib") { memoizedFib(40) }) }
      }
   }

   job.join()

}

缓存函数内部使用线程安全的结构来存储其结果,因此可以在协程或任何其他并发代码上安全使用。

使用缓存函数存在潜在缺点。第一个缺点是读取内部缓存的进程比实际计算或内存消耗要高,因为目前,缓存函数不暴露任何行为来控制其内部存储。

部分函数

部分函数(不要与部分应用函数混淆)是一个对于其参数类型的每个可能值不一定有定义的函数。相比之下,总函数是对于其参数类型的每个可能值都有定义的函数。

让我们看看以下示例:

fun main(args: Array<String>) {
   val upper: (String?) -> String = { s:String? -> s!!.toUpperCase()} //Partial function, it can't transform null

   listOf("one", "two", null, "four").map(upper).forEach(::println) //NPE
}

upper 函数是一个部分函数;尽管 null 是一个有效的 String? 值,但它不能处理 null 值。如果你尝试运行此代码,它将抛出 NullPointerExceptionNPE)。

Arrow 为类型 (T) -> R 的部分函数提供了一个显式类型 PartialFunction<T, R>

import arrow.core.PartialFunction

fun main(args: Array<String>) {
 val upper: (String?) -> String = { s: String? -> s!!.toUpperCase() } //Partial function, it can't transform null

 val partialUpper: PartialFunction<String?, String> = PartialFunction(definetAt = { s -> s != null }, f = upper)

 listOf("one", "two", null, "four").map(partialUpper).forEach(::println) //IAE: Value: (null) isn't supported by this function 
}

PartialFunction<T, R> 接收一个谓词 (T) -> Boolean 作为第一个参数,该参数必须返回 true,如果函数对该特定值有定义。PartialFunction<T, R> 函数扩展自 (T) -> R,因此它可以作为一个普通函数使用。

在这个例子中,代码仍然抛出异常,但现在类型为 IllegalArgumentExceptionIAE),并带有有用的消息。

为了避免抛出异常,我们必须将我们的部分函数转换为总函数:

fun main(args: Array<String>) {

   val upper: (String?) -> String = { s: String? -> s!!.toUpperCase() } //Partial function, it can't transform null

   val partialUpper: PartialFunction<String?, String> = PartialFunction(definetAt = { s -> s != null }, f = upper)

   listOf("one", "two", null, "four").map{ s -> partialUpper.invokeOrElse(s, "NULL")}.forEach(::println)
}

一种选择是使用 invokeOrElse 函数,在值 s 对此函数未定义时返回默认值:

fun main(args: Array<String>) {

   val upper: (String?) -> String = { s: String? -> s!!.toUpperCase() } //Partial function, it can't transform null

   val partialUpper: PartialFunction<String?, String> = PartialFunction(definetAt = { s -> s != null }, f = upper)

   val upperForNull: PartialFunction<String?, String> = PartialFunction({ s -> s == null }) { "NULL" }

   val totalUpper: PartialFunction<String?, String> = partialUpper orElse upperForNull

   listOf("one", "two", null, "four").map(totalUpper).forEach(::println)
}

第二种选择是使用 orElse 函数创建一个总函数,该函数由几个部分函数组成:

fun main(args: Array<String>) {
   val fizz = PartialFunction({ n: Int -> n % 3 == 0 }) { "FIZZ" }
   val buzz = PartialFunction({ n: Int -> n % 5 == 0 }) { "BUZZ" }
   val fizzBuzz = PartialFunction({ n: Int -> fizz.isDefinedAt(n) && buzz.isDefinedAt(n) }) { "FIZZBUZZ" }
   val pass = PartialFunction({ true }) { n: Int -> n.toString() }

   (1..50).map(fizzBuzz orElse buzz orElse fizz orElse pass).forEach(::println)
}

通过isDefinedAt(T)函数,我们可以重用内部谓词,在这种情况下,用于构建fizzBuzz的条件。当在orElse链中使用时,声明顺序具有优先权,首先为某个值定义的部分函数将被执行,而链中的其他函数将被忽略。

身份和常量

身份和常量是简单的函数。identity函数返回提供的参数值;类似于加法和乘法恒等性质,将 0 加到任何数上仍然是同一个数。

constant<T, R>(t: T)函数返回一个新函数,该函数将始终返回t值:

fun main(args: Array<String>) {

   val oneToFour = 1..4

   println("With identity: ${oneToFour.map(::identity).joinToString()}") //1, 2, 3, 4

   println("With constant: ${oneToFour.map(constant(1)).joinToString()}") //1, 1, 1, 1

}

我们可以使用constant重新编写我们的fizzBuzz值:

fun main(args: Array<String>) {
   val fizz = PartialFunction({ n: Int -> n % 3 == 0 }, constant("FIZZ"))
   val buzz = PartialFunction({ n: Int -> n % 5 == 0 }, constant("BUZZ"))
   val fizzBuzz = PartialFunction({ n: Int -> fizz.isDefinedAt(n) && buzz.isDefinedAt(n) }, constant("FIZZBUZZ"))
   val pass = PartialFunction<Int, String>(constant(true)) { n -> n.toString() }

   (1..50).map(fizzBuzz orElse buzz orElse fizz orElse pass).forEach(::println)
}

身份和常量函数在函数式编程或数学算法的实现中非常有用,例如,常量是 SKI 组合演算中的 K。

透镜

透镜是优雅地更新不可变数据结构的抽象。透镜的一种形式是Lens(或透镜,具体取决于库实现)。Lens是一个功能引用,可以聚焦(因此得名)到结构中,并读取、写入或修改其目标:

typealias GB = Int

data class Memory(val size: GB)
data class MotherBoard(val brand: String, val memory: Memory)
data class Laptop(val price: Double, val motherBoard: MotherBoard)

fun main(args: Array<String>) {
   val laptopX8 = Laptop(500.0, MotherBoard("X", Memory(8)))

   val laptopX16 = laptopX8.copy(
         price = 780.0,
         motherBoard = laptopX8.motherBoard.copy(
               memory = laptopX8.motherBoard.memory.copy(
                     size = laptopX8.motherBoard.memory.size * 2
               )
         )
   )

   println("laptopX16 = $laptopX16")
}

要从现有值创建一个新的Laptop值,我们需要使用多个嵌套的复制方法和引用。在这个例子中,这并不那么糟糕,但您可以想象在一个更复杂的数据结构中,事情可能会变得混乱。

让我们编写我们非常第一个Lens值:

val laptopPrice: Lens<Laptop, Double> = Lens(
      get = { laptop -> laptop.price },
      set = { price -> { laptop -> laptop.copy(price = price) } }
)

laptopPrice值是一个Lens<Laptop, Double>,我们使用函数Lens<S, T, A, B>(实际上是Lens.invoke)初始化它。Lens接受两个函数作为参数,作为get: (S) -> Aset: (B) -> (S) -> T

如您所见,set是一个柯里化函数,因此您可以像这样编写您的设置:

import arrow.optics.Lens

val laptopPrice: Lens<Laptop, Double> = Lens(
      get = { laptop -> laptop.price },
      set = { price: Double, laptop: Laptop -> laptop.copy(price = price) }.curried()
)

根据您的偏好,这可以使阅读和编写变得更加容易。

现在您已经拥有了第一个透镜,它可以用来设置、读取和修改笔记本电脑的价格。这并不太令人印象深刻,但透镜的魔力在于它们的组合:

import arrow.optics.modify

val laptopMotherBoard: Lens<Laptop, MotherBoard> = Lens(
      get = { laptop -> laptop.motherBoard },
      set = { mb -> { laptop -> laptop.copy(motherBoard = mb) } }
)

val motherBoardMemory: Lens<MotherBoard, Memory> = Lens(
      get = { mb -> mb.memory },
      set = { memory -> { mb -> mb.copy(memory = memory) } }
)

val memorySize: Lens<Memory, GB> = Lens(
      get = { memory -> memory.size },
      set = { size -> { memory -> memory.copy(size = size) } }
)

fun main(args: Array<String>) {
   val laptopX8 = Laptop(500.0, MotherBoard("X", Memory(8)))

   val laptopMemorySize: Lens<Laptop, GB> = laptopMotherBoard compose motherBoardMemory compose memorySize

   val laptopX16 = laptopMemorySize.modify(laptopPrice.set(laptopX8, 780.0)) { size ->
      size * 2
   }

   println("laptopX16 = $laptopX16")
}

我们通过将LaptopmemorySize的所有透镜组合创建了laptopMemorySize;然后,我们可以设置笔记本电脑的价格并修改其内存。

尽管透镜很酷,但看起来有很多样板代码。不用担心,Arrow 可以为您生成这些透镜。

配置 Arrows 代码生成

在一个 Gradle 项目中,添加一个名为generated-kotlin-sources.gradle的文件:

apply plugin: 'idea'

idea {
    module {
        sourceDirs += files(
            'build/generated/source/kapt/main',
            'build/generated/source/kaptKotlin/main',
            'build/tmp/kapt/main/kotlinGenerated')
        generatedSourceDirs += files(
            'build/generated/source/kapt/main',
            'build/generated/source/kaptKotlin/main',
            'build/tmp/kapt/main/kotlinGenerated')
    }
}

然后,在build.gradle文件中,添加以下内容:

apply plugin: 'kotlin-kapt'
apply from: rootProject.file('gradle/generated-kotlin-sources.gradle')

build.gradle文件中添加一个新的依赖项:

dependencies {
    ...
    kapt    'io.arrow-kt:arrow-annotations-processor:0.5.2'
    ...     
}

一旦配置完成,您就可以使用常规的构建命令生成 Arrow 代码,./gradlew build

生成透镜

一旦配置了 Arrow 的代码生成,您可以将@lenses注解添加到您希望生成透镜的数据类中:

import arrow.lenses
import arrow.optics.Lens
import arrow.optics.modify

typealias GB = Int

@lenses data class Memory(val size: GB)
@lenses data class MotherBoard(val brand: String, val memory: Memory)
@lenses data class Laptop(val price: Double, val motherBoard: MotherBoard)

fun main(args: Array<String>) {
   val laptopX8 = Laptop(500.0, MotherBoard("X", Memory(8)))

   val laptopMemorySize: Lens<Laptop, GB> = laptopMotherBoard() compose motherBoardMemory() compose memorySize()

   val laptopX16 = laptopMemorySize.modify(laptopPrice().set(laptopX8, 780.0)) { size ->
      size * 2
   }

   println("laptopX16 = $laptopX16")
}

Arrow 为我们的数据类生成与构造函数参数数量一样多的透镜,名称约定为classProperty,并在同一包中,因此不需要额外的导入。

摘要

在本章中,我们介绍了 Arrow 的许多特性,这些特性为我们提供了创建、生成和丰富现有函数的工具。我们使用现有的函数组合成新的函数;我们包括了部分应用和柯里化。我们还通过记忆化缓存了纯函数的结果,并使用透镜修改了数据结构。

Arrow 的特性为使用基本函数式原则创建丰富且可维护的应用程序打开了可能性。

在下一章中,我们将介绍更多的 Arrow 特性,包括OptionEitherTry等数据类型。

第十三章:箭头类型

Arrow 包含了许多传统函数类型的实现,如 OptionEitherTry,以及许多其他类型类,如 functormonad

在本章中,我们将涵盖以下主题:

  • 使用 Option 来管理空值

  • EitherTry 用于管理错误

  • 组合和转换器

  • State 用于管理应用程序状态

选项

Option<T> 数据类型是值 T 的存在或不存在的一种表示。在 Arrow 中,Option<T> 是一个密封类,有两个子类型,Some<T>,一个表示值 T 存在的数据类,以及 None,一个表示值不存在的一个对象。作为一个密封类,Option<T> 不能有其他子类型;因此编译器可以全面检查,如果 Some<T>None 都被覆盖。

我知道(或者假装知道)你此刻的想法——为什么我需要 Option<T> 来表示 T 的存在或不存在,如果 Kotlin 中已经有 T 表示存在,T? 表示不存在呢?

你是对的。但是 Option 提供的比可空类型更多的价值,让我们直接跳到例子:

fun divide(num: Int, den: Int): Int? {
    return if (num % den != 0) {
        null
    } else {
        num / den
    }
}

fun division(a: Int, b: Int, den: Int): Pair<Int, Int>? {
    val aDiv = divide(a, den)
    return when (aDiv) {
        is Int -> {
            val bDiv = divide(b, den)
            when (bDiv) {
                is Int -> aDiv to bDiv
                else -> null
            }
        }
        else -> null
    }
}

division 函数接受三个参数——两个整数(ab)和一个除数(den),如果两个数都能被 den 整除,则返回一个 Pair<Int, Int>,否则返回 null

我们可以用 Option 表达相同的算法:

import arrow.core.*
import arrow.syntax.option.toOption

fun optionDivide(num: Int, den: Int): Option<Int> = divide(num, den).toOption()

fun optionDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
   val aDiv = optionDivide(a, den)
   return when (aDiv) {
      is Some -> {
         val bDiv = optionDivide(b, den)
         when (bDiv) {
            is Some -> Some(aDiv.t to bDiv.t)
            else -> None
         }
      }
      else -> None
   }
}

函数 optionDivide 接受除法操作的空值结果,并使用 toOption() 扩展函数将其作为 Option 返回。

division 相比,optionDivision 没有太大变化,它是以不同类型表达的同一种算法。如果我们在这里停止,那么 Option<T> 在可空类型之上并没有提供额外的价值。幸运的是,情况并非如此;有更多使用 Option 的方法:

fun flatMapDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
   return optionDivide(a, den).flatMap { aDiv: Int ->
      optionDivide(b, den).flatMap { bDiv: Int ->
         Some(aDiv to bDiv)
      }
   }
}

Option 提供了几个函数来处理其内部值,在这个例子中,flatMap(作为一个 monad)以及现在我们的代码看起来要短得多。

看看以下简短列表中的一些 Option<T> 函数:

函数 描述
exists(p :Predicate<T>): Boolean 如果存在值 T,则返回谓词 p 的结果,否则返回 null。
filter(p: Predicate<T>): Option<T> 如果值 T 存在并且满足谓词 p,则返回 Some<T>,否则返回 None
flatMap(f: (T) -> Option<T>): Option<T> 一个 flatMap 转换函数(类似于 monad)。
<R> fold(ifEmpty: () -> R, some: (T) -> R): R<R> 返回值被转换为 R,对于 None 调用 ifEmpty,对于 Some<T> 调用 some
getOrElse(default:() -> T): T 如果存在值 T,则返回值 T,否则返回 default 结果。
<R> map(f: (T) -> R):Option<T> 一个转换函数(类似于 functor)。
orNull(): T? 将值 T 返回为可空的 T?

最后的除法实现将使用列表推导式:

import arrow.typeclasses.binding

fun comprehensionDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
   return Option.monad().binding {
      val aDiv: Int = optionDivide(a, den).bind()
      val bDiv: Int = optionDivide(b, den).bind()
      aDiv to bDiv
   }.ev()
}

组合是一种计算技术,可以按顺序计算任何包含 flatMap 函数并能提供 monad 实例的类型(例如 OptionList 等)。

在 Arrow 中,使用协程进行组合。是的,协程在异步执行域之外也很有用。

如果我们将之前示例中的延续进行概述,它将看起来像这样(这是一个有助于理解协程的有用心理模型)

fun comprehensionDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
   return Option.monad().binding {
      val aDiv: Int = optionDivide(a, den).bind()
      // start continuation 1
         val bDiv: Int = optionDivide(b, den).bind()
          //start continuation 2
            aDiv to bDiv
         //end continuation 2
 // end continuation 1
   }.ev()
}

Option.monad().binding 是一个协程构建器,而 bind() 函数是一个挂起函数。如果你正确地回忆起我们的协程章节,延续是任何挂起点之后(即挂起函数被调用时)的代码表示。在我们的示例中,我们有两个挂起点和两个延续,当我们返回(在最后一行块中)时,我们处于第二个延续中,我们可以访问两个值,aDivbDiv

将此算法作为延续来阅读与我们的 flatMapDivision 函数非常相似。在幕后,Option.monad().binding 使用带有延续的 Option.flatMap 来创建组合;一旦编译,comprehensionDivisionflatMapDivision 在很大程度上是等价的。

ev() 方法将在下一节中解释。

Arrow 的类型层次结构

Kotlin 的类型系统存在一个限制——它不支持高阶类型HKT)。不深入类型理论的话,HKT 是一种声明其他泛型值作为类型参数的类型:

class MyClass<T>() //Valid Kotlin code

class MyHigherKindedClass<K<T>>() //Not valid kotlin code

对于 Kotlin 的函数式编程来说,缺少 HKT 并不是一件好事,因为许多高级函数式构造和模式都使用了它们。

Arrow 团队正在致力于Kotlin 进化和增强过程KEEP)——这是一个社区流程,用于添加新的语言特性,在 Kotlin 中称为类型类扩展(github.com/Kotlin/KEEP/pull/87)以支持 HKT 和其他特性。目前,尚不清楚这个 KEEP(代码为 KEEP-87)何时会被纳入 Kotlin,但到目前为止,这是最受评论的提案,并吸引了大量关注。由于它仍在进行中,细节尚不明确,但其中透露出一丝希望。

Arrow 解决这个问题的方法是通过对称为基于证据的 HKTs 的技术进行模拟。

让我们看看一个 Option<T> 的声明:

package arrow.core

import arrow.higherkind
import java.util.*

/**
 * Represents optional values. Instances of `Option`
 * are either an instance of $some or the object $none.
 */
@higherkind
sealed class Option<out A> : OptionKind<A> {
  //more code goes here

Option<A> 使用了 @higherkind 注解,这与我们上一章中提到的 @lenses 类似;这个注解用于生成支持基于证据的 HKTs 的代码。Option<A>OptionKind<A> 扩展而来:

package arrow.core

class OptionHK private constructor()
typealias OptionKind<A> = arrow.HK<OptionHK, A>

@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
inline fun <A> OptionKind<A>.ev(): Option<A> =
  this as Option<A>

OptionKind<A>HK<OptionHK, A> 的类型别名,所有这些代码都是使用 @higherkind 注解处理器生成的。OptionHK 是一个不可实例化的类,用作 HKOptionKind 的唯一标签名称,而 OptionKind 是 HKT 的一种中间表示形式。Option.monad().binding 返回 OptionKind<T>,这就是为什么我们需要在最后调用 ev() 来返回一个合适的 Option<T>

package arrow

interface HK<out F, out A>

typealias HK2<F, A, B> = HK<HK<F, A>, B>

typealias HK3<F, A, B, C> = HK<HK2<F, A, B>, C>

typealias HK4<F, A, B, C, D> = HK<HK3<F, A, B, C>, D>

typealias HK5<F, A, B, C, D, E> = HK<HK4<F, A, B, C, D>, E>

HK 接口(higher-kinded 的简称)用于表示一元到 HK5 的 HKT,其中 HK5 表示五元。在 HK<F, A> 上,F 表示类型,A 表示泛型参数,因此 Option<Int>OptionKind<Int> 值,它是 HK<OptionHK, Int>

让我们看看 Functor<F>

package arrow.typeclasses

import arrow.*

@typeclass
interface Functor<F> : TC {

    fun <A, B> map(fa: HK<F, A>, f: (A) -> B): HK<F, B>

}

Functor<F> 扩展 TC,一个标记接口,并且,正如你可以猜到的,它有一个 map 函数。map 函数接收 HK<F, A> 作为第一个参数,并接收一个 lambda (A) -> B 来将 A 的值转换为 B,并将其转换为 HK<F, B>

让我们创建我们的基本数据类型 Mappable,它可以提供 Functor 类型类的实例:

import arrow.higherkind

@higherkind
class Mappable<T>(val t: T) : MappableKind<T> {
   fun <R> map(f: (T) -> R): Mappable<R> = Mappable(f(t))

   override fun toString(): String = "Mappable(t=$t)"

   companion object
}

我们类 Mappable<T>@higherkind 注解,并扩展 MappableKind<T>,必须有一个伴随对象,无论它是空的还是非空的。

现在,我们需要创建我们的 Functor<F> 实现:

import arrow.instance
import arrow.typeclasses.Functor

@instance(Mappable::class)
interface MappableFunctorInstance : Functor<MappableHK> {
   override fun <A, B> map(fa: MappableKind<A>, f: (A) -> B): Mappable<B> {
      return fa.ev().map(f)
   }
}

我们的 MappableFunctorInstance 接口扩展 Functor<MappableHK> 并用 @instance(Mappable::class) 注解。在 map 函数内部,我们使用第一个参数 MappableKind<A> 并使用其 map 函数。

@instance 注解将生成一个扩展接口的对象,MappableFunctorInstance。它将创建一个 Mappable.Companion.functor() 扩展函数,使用 Mappable.functor()(这是我们如何使用 Option.monad() 的方式)来获取实现 MappableFunctorInstance 的对象。

另一个替代方案是让 Arrow 衍生的实例自动提供,前提是你的数据类型具有正确的函数:

import arrow.deriving 

@higherkind
@deriving(Functor::class)
class DerivedMappable<T>(val t: T) : DerivedMappableKind<T> {
   fun <R> map(f: (T) -> R): DerivedMappable<R> = DerivedMappable(f(t))

   override fun toString(): String = "DerivedMappable(t=$t)"

   companion object
}

@deriving 注解将生成 DerivedMappableFunctorInstance,这通常是你手动编写的。

现在,我们可以创建一个泛型函数来使用我们的 Mappable 函子:

import arrow.typeclasses.functor

inline fun <reified F> buildBicycle(mapper: HK<F, Int>,
                           noinline f: (Int) -> Bicycle,
                           FR: Functor<F> = functor()): HK<F, Bicycle> = FR.map(mapper, f)

buildBicycle 函数将接受任何 HK<F, Int> 作为参数,并使用由 arrow.typeclasses.functor 函数返回的 Functor 实现应用函数 f,并返回 HK<F, Bicycle>

函数 arrow.typeclass.functor 在运行时解析,符合 Functor<MappableHK> 要求的实例:

fun main(args: Array<String>) {

   val mappable: Mappable<Bicycle> = buildBicycle(Mappable(3), ::Bicycle).ev()
   println("mappable = $mappable") //Mappable(t=Bicycle(gears=3))

   val option: Option<Bicycle> = buildBicycle(Some(2), ::Bicycle).ev()
   println("option = $option") //Some(Bicycle(gears=2))

   val none: Option<Bicycle> = buildBicycle(None, ::Bicycle).ev()
   println("none = $none") //None

}

我们可以使用 buildBicycleMappeable<Int> 或任何其他 HKT 类,例如 Option<T>

使用箭头方法处理 HKT 的问题之一是它必须在运行时解析其实例。这是因为 Kotlin 没有对隐式或编译时解决类型类实例的支持,这使得 Arrow 只能选择这个替代方案,直到 KEEP-87 被批准并包含在语言中:

@higherkind
class NotAFunctor<T>(val t: T) : NotAFunctorKind<T> {
   fun <R> map(f: (T) -> R): NotAFunctor<R> = NotAFunctor(f(t))

   override fun toString(): String = "NotAFunctor(t=$t)"
}

因此,你可以有一个具有 map 函数的 HKT,但没有 Functor 实例无法使用,但这并不是编译错误:

fun main(args: Array<String>) {

   val not: NotAFunctor<Bicycle> = buildBicycle(NotAFunctor(4), ::Bicycle).ev()
   println("not = $not")

}

使用 NotAFunctor<T> 函数调用 buildBicycle 会编译,但在运行时将抛出 ClassNotFoundException 异常。

现在我们已经了解了 Arrow 的层次结构如何工作,我们可以介绍其他类。

Either

Either<L, R> 表示两种可能值之一 LR,但不是同时表示。Either 是一个密封类(类似于 Option),有两个子类型 Left<L>Right<R>。通常 Either 用于表示可能失败的结果,使用左侧表示错误,右侧表示成功的结果。因为表示可能失败的操作是常见场景,Arrow 的 Either 是右偏的,换句话说,除非有其他说明,否则所有操作都在右侧运行。

让我们将我们的除法示例从 Option 转换为 Either

import arrow.core.Either
import arrow.core.Either.Right
import arrow.core.Either.Left

fun eitherDivide(num: Int, den: Int): Either<String, Int> {
   val option = optionDivide(num, den)
   return when (option) {
      is Some -> Right(option.t)
      None -> Left("$num isn't divisible by $den")
   }
}

现在而不是返回一个 None 值,我们正在向用户返回有价值的信息:

import arrow.core.Tuple2

fun eitherDivision(a: Int, b: Int, den: Int): Either<String, Tuple2<Int, Int>> {
   val aDiv = eitherDivide(a, den)
   return when (aDiv) {
      is Right -> {
         val bDiv = eitherDivide(b, den)
         when (bDiv) {
            is Right -> Right(aDiv.getOrElse { 0 } toT bDiv.getOrElse { 0 })
            is Left -> bDiv as Either<String, Nothing>
         }
      }
      is Left -> aDiv as Either<String, Nothing>
   }
}

eitherDivision 中,我们使用 Arrow 的 Tuple<A, B> 而不是 Kotlin 的 Pair<A, B>。元组比 Pair/Triple 提供更多功能,从现在开始我们将使用它。要创建 Tuple2,可以使用扩展 infix 函数,toT

接下来,是一个 Either<L, R> 函数的简短列表:

函数 描述
bimap(fa:(L) -> T, fb:(R) -> X): Either<T, X> 使用 faLeft 进行转换,使用 fbRight 进行转换,以返回 Either<T, X>
contains(elem:R): Boolean 如果 Right 值与 elem 参数相同,则返回 true,对于 Left 返回 false
exists(p:Predicate<R>):Boolean 如果 Right,则返回谓词 p 的结果,对于 Left 总是返回 false
flatMap(f: (R) -> Either<L, T>): Either<L, T> Monad 中的 flatMap 函数类似,使用 Right 的值。
fold(fa: (L) -> T, fb: (R) -> T): T 执行 faLeftfbRight 返回 T 值。
getOrElse(default:(L) -> R): R 返回 Right 值,或 default 函数的结果。
isLeft(): Boolean 如果是 Left 的实例,则返回 true,对于 Right 返回 false
isRight(): Boolean 如果是 Right 的实例,则返回 true,对于 Left 返回 false
map(f: (R) -> T): Either<L, T> Functor 中的 map 函数类似,如果 Right,则使用函数 f 将其转换为 Right<T>,如果 Left,则返回相同值而不进行转换。
mapLeft(f: (L) -> T): Either<T, R> Functor 中的 map 函数类似,如果 Left,则使用函数 f 将其转换为 Left<T>,如果 Right,则返回相同值而不进行转换。
swap(): Either<R, L> 返回类型和值互换的 Either
toOption(): Option<R> 对于 Right 返回 Some<T>,对于 Left 返回 None

flatMap 版本看起来符合预期:

fun flatMapEitherDivision(a: Int, b: Int, den: Int): Either<String, Tuple2<Int, Int>> {
   return eitherDivide(a, den).flatMap { aDiv ->
      eitherDivide(b, den).flatMap { bDiv ->
         Right(aDiv toT bDiv)
      }
   }
}

Either 具有单子实现,因此我们可以调用绑定函数:

fun comprehensionEitherDivision(a: Int, b: Int, den: Int): Either<String, Tuple2<Int, Int>> {
   return Either.monad<String>().binding {
      val aDiv = eitherDivide(a, den).bind()
      val bDiv = eitherDivide(b, den).bind()

      aDiv toT bDiv
   }.ev()

注意 Either.monad<L>();对于 Either<L, R>,它必须定义 L 类型:

fun main(args: Array<String>) {
   eitherDivision(3, 2, 4).fold(::println, ::println) //3 isn't divisible by 4
}

在下一节中,我们将学习单子变换器。

单子变换器

EitherOption使用简单,但如果我们将两者结合会发生什么呢?

object UserService {

   fun findAge(user: String): Either<String, Option<Int>> {
       //Magic  
   }
}

UserService.findAge返回Either<String, Option<Int>>Left<String>表示访问数据库或其他基础设施时出错,Right<None>表示数据库中没有找到值,Right<Some<Int>>表示找到了值:

import arrow.core.*
import arrow.syntax.function.pipe

fun main(args: Array<String>) {
 val anakinAge: Either<String, Option<Int>> = UserService.findAge("Anakin")

 anakinAge.fold(::identity, { op ->
         op.fold({ "Not found" }, Int::toString)
     }) pipe ::println 
}

要打印年龄,我们需要两个嵌套的折叠,没有什么太复杂的。问题出现在我们需要执行访问多个值的操作时:

import arrow.core.*
import arrow.syntax.function.pipe
import kotlin.math.absoluteValue

fun main(args: Array<String>) {
   val anakinAge: Either<String, Option<Int>> = UserService.findAge("Anakin")
   val padmeAge: Either<String, Option<Int>> = UserService.findAge("Padme")

   val difference: Either<String, Option<Either<String, Option<Int>>>> = anakinAge.map { aOp ->
      aOp.map { a ->
         padmeAge.map { pOp ->
            pOp.map { p ->
               (a - p).absoluteValue
            }
         }
      }
   }

   difference.fold(::identity, { op1 ->
      op1.fold({ "Not Found" }, { either ->
         either.fold(::identity, { op2 -> 
            op2.fold({ "Not Found" }, Int::toString) })
      })
   }) pipe ::println
}

摩纳哥不组合,这使得这些操作很快就会变得复杂。但是,我们总是可以依赖列表解析,不是吗?现在,让我们看看以下代码:

import arrow.core.*
import arrow.syntax.function.pipe
import arrow.typeclasses.binding
import kotlin.math.absoluteValue

fun main(args: Array<String>) {
   val anakinAge: Either<String, Option<Int>> = UserService.findAge("Anakin")
   val padmeAge: Either<String, Option<Int>> = UserService.findAge("Padme")

   val difference: Either<String, Option<Option<Int>>> = Either.monad<String>().binding {
      val aOp: Option<Int> = anakinAge.bind()
      val pOp: Option<Int> = padmeAge.bind()
      aOp.map { a ->
         pOp.map { p ->
            (a - p).absoluteValue
         }
      }
   }.ev()

   difference.fold(::identity, { op1 ->
      op1.fold({ "Not found" }, { op2 ->
         op2.fold({ "Not found" }, Int::toString) }) }) pipe ::println
}

这样更好,返回类型不那么长,fold也更易于管理。让我们看看以下代码片段中的嵌套列表解析:

fun main(args: Array<String>) {
   val anakinAge: Either<String, Option<Int>> = UserService.findAge("Anakin")
   val padmeAge:  Either<String, Option<Int>> = UserService.findAge("Padme")

   val difference: Either<String, Option<Int>> = Either.monad<String>().binding {
      val aOp: Option<Int> = anakinAge.bind()
      val pOp: Option<Int> = padmeAge.bind()
      Option.monad().binding {
         val a: Int = aOp.bind()
         val p: Int = pOp.bind()
         (a - p).absoluteValue
      }.ev()
   }.ev()

   difference.fold(::identity, { op ->
      op.fold({ "Not found" }, Int::toString)
   }) pipe ::println
}

现在,我们有两个值和结果具有相同的类型。但我们还有一个选择,摩纳哥转换器。

摩纳哥转换器是两个摩纳哥的组合,可以作为一个整体执行。在我们的例子中,我们将使用OptionTOption Transformer的缩写),因为Option是嵌套在Either内部的摩纳哥类型:

import arrow.core.*
import arrow.data.OptionT
import arrow.data.monad
import arrow.data.value
import arrow.syntax.function.pipe
import arrow.typeclasses.binding
import kotlin.math.absoluteValue

fun main(args: Array<String>) {
   val anakinAge: Either<String, Option<Int>> = UserService.findAge("Anakin")
   val padmeAge: Either<String, Option<Int>> = UserService.findAge("Padme")

   val difference: Either<String, Option<Int>> = OptionT.monad<EitherKindPartial<String>>().binding {
      val a: Int = OptionT(anakinAge).bind()
      val p: Int = OptionT(padmeAge).bind()
      (a - p).absoluteValue
   }.value().ev()

   difference.fold(::identity, { op ->
      op.fold({ "Not found" }, Int::toString)
   }) pipe ::println
}

我们使用OptionT.monad<EitherKindPartial<String>>().bindingEitherKindPartial<String>摩纳哥意味着包装类型是Either<String, Option<T>>

binding块内部,我们使用OptionT对类型为Either<String, Option<T>>(技术上是对类型为HK<HK<EitherHK, String>, Option<T>>的值)的值调用bind(): T,在我们的例子中TInt

之前我们只使用了ev()方法,但现在我们需要使用value()方法来提取OptionT内部值。

在下一节中,我们将学习关于Try类型的内容。

Try

Try表示可能成功或失败的计算。Try<A>是一个密封类,有两个可能的子类—Failure<A>,表示失败,Success<T>表示成功操作。

让我们用Try来写我们的除法示例:

import arrow.data.Try

fun tryDivide(num: Int, den: Int): Try<Int> = Try { divide(num, den)!! }

创建Try实例的最简单方法是使用Try.invoke操作符。如果块内部抛出异常,它将返回Failure;如果一切顺利,例如返回Success<Int>,则!!操作符将抛出NPE,如果除法返回 null:

fun tryDivision(a: Int, b: Int, den: Int): Try<Tuple2<Int, Int>> {
   val aDiv = tryDivide(a, den)
   return when (aDiv) {
      is Success -> {
         val bDiv = tryDivide(b, den)
         when (bDiv) {
            is Success -> {
               Try { aDiv.value toT bDiv.value }
            }
            is Failure -> Failure(bDiv.exception)
         }
      }
      is Failure -> Failure(aDiv.exception)
   }
}

让我们看看Try<T>函数的简短列表:

函数 描述
exists(p: Predicate<T>): Boolean 如果Success<T>返回p结果,在Failure时总是返回false
filter(p: Predicate<T>): Try<T> 如果操作成功并且通过谓词p,则返回Success<T>,否则返回Failure
<R> flatMap(f: (T) -> Try<R>): Try<R> flatMap函数,类似于摩纳哥。
<R> fold(fa: (Throwable) -> R, fb:(T) -> R): R 返回值转换为R,如果为Failure则调用fa
getOrDefault(default: () -> T): T 返回值T,如果为Failure则调用默认值。
getOrElse(default: (Throwable) -> T): T 返回值T,如果为Failure则调用默认值。
isFailure(): Boolean 如果是 Failure 则返回 true,否则返回 false
isSuccess(): Boolean 如果是 Success 则返回 true,否则返回 false
<R> map(f: (T) -> R): Try<R> 如同在函子中转换函数。
onFailure(f: (Throwable) -> Unit): Try<T> Failure 上执行操作。
onSuccess(f: (T) -> Unit): Try<T> Success 上执行操作。
orElse(f: () -> Try<T>): Try<T> Success 上返回自身或在 Failure 上返回 f 的结果。
recover(f: (Throwable) -> T): Try<T> 转换 map 函数用于 Failure
recoverWith(f: (Throwable) -> Try<T>): Try<T> 转换 flatMap 函数用于 Failure
toEither() : Either<Throwable, T> 转换为 Either——Failure 转换为 Left<Throwable>Success<T> 转换为 Right<T>
toOption(): Option<T> 转换为 Option——Failure 转换为 NoneSuccess<T> 转换为 Some<T>

flatMap 的实现与 EitherOption 非常相似,展示了拥有一个共同的命名和行为约定的价值:

fun flatMapTryDivision(a: Int, b: Int, den: Int): Try<Tuple2<Int, Int>> {
   return tryDivide(a, den).flatMap { aDiv ->
      tryDivide(b, den).flatMap { bDiv ->
         Try { aDiv toT bDiv }
      }
   }
}

Monadic 理解也适用于 Try

fun comprehensionTryDivision(a: Int, b: Int, den: Int): Try<Tuple2<Int, Int>> {
   return Try.monad().binding {
      val aDiv = tryDivide(a, den).bind()
      val bDiv = tryDivide(b, den).bind()
      aDiv toT bDiv
   }.ev()
}

另一种使用 MonadError 实例的 monadic 理解:

fun monadErrorTryDivision(a: Int, b: Int, den: Int): Try<Tuple2<Int, Int>> {
   return Try.monadError().bindingCatch {
      val aDiv = divide(a, den)!!
      val bDiv = divide(b, den)!!
      aDiv toT bDiv
   }.ev()
}

使用 monadError.bindingCatch,任何抛出异常的操作都会提升到 Failure,最后返回值会被包裹在 Try<T> 中。MonadError 也适用于 OptionEither

State

State 是一种提供函数式处理应用程序状态的结构的结构。State<S, A>S -> Tuple2<S, A> 的抽象。S 代表状态类型,Tuple2<S, A> 是结果,其中 S 是新更新的状态,A 是函数返回值。

我们可以从一个简单的例子开始,一个返回两个东西的函数,一个价格和计算它的步骤。为了计算价格,我们需要加上 20% 的 VAT,如果 price 值超过某个阈值,则应用折扣:

import arrow.core.Tuple2
import arrow.core.toT
import arrow.data.State

typealias PriceLog = MutableList<Tuple2<String, Double>>

fun addVat(): State<PriceLog, Unit> = State { log: PriceLog ->
    val (_, price) = log.last()
    val vat = price * 0.2
    log.add("Add VAT: $vat" toT price + vat)
    log toT Unit
}

我们有一个类型别名 PriceLog 用于 MutableList<Tuple2<String, Double>>PriceLog 将是我们的 State 表示;每个步骤都用 Tuple2<String, Double> 表示。

我们的第一个函数 addVat(): State<PriceLog, Unit> 表示第一步。我们使用 State 构建器编写这个函数,它接收 PriceLog,在应用任何步骤之前的初始状态,并必须返回一个 Tuple2<PriceLog, Unit>,我们使用 Unit 因为在这个点上我们不需要价格:

fun applyDiscount(threshold: Double, discount: Double): State<PriceLog, Unit> = State { log ->
    val (_, price) = log.last()
    if (price > threshold) {
        log.add("Applying -$discount" toT price - discount)
    } else {
        log.add("No discount applied" toT price)
    }
    log toT Unit
}

applyDiscount 函数是我们的第二步。我们在这里引入的唯一新元素是两个参数,一个用于 threshold,另一个用于 discount

fun finalPrice(): State<PriceLog, Double> = State { log ->
    val (_, price) = log.last()
    log.add("Final Price" toT price)
    log toT price
}

最后一步由函数 finalPrice() 表示,现在我们返回 Double 而不是 Unit

import arrow.data.ev
import arrow.instances.monad
import arrow.typeclasses.binding

fun calculatePrice(threshold: Double, discount: Double) = State().monad<PriceLog>().binding {
    addVat().bind() //Unit
    applyDiscount(threshold, discount).bind() //Unit
    val price: Double = finalPrice().bind()
    price
}.ev()

为了表示步骤序列,我们使用 monadic 理解并按顺序使用 State 函数。从一个函数到下一个函数,PriceLog 状态隐式流动(只是某些协程连续性的魔法)。最后,我们产生最终价格。添加新步骤或切换现有步骤就像添加或移动行一样简单:

import arrow.data.run
import arrow.data.runA

fun main(args: Array<String>) {
    val (history: PriceLog, price: Double) = calculatePrice(100.0, 2.0).run(mutableListOf("Init" toT 15.0))
    println("Price: $price")
    println("::History::")
    history
            .map { (text, value) -> "$text\t|\t$value" }
            .forEach(::println)

    val bigPrice: Double = calculatePrice(100.0, 2.0).runA(mutableListOf("Init" toT 1000.0))
    println("bigPrice = $bigPrice")
}

要使用 calculatePrice 函数,你必须提供阈值和折扣值,然后使用初始状态调用扩展函数 run。如果你只对价格感兴趣,可以使用 runA,或者只对历史感兴趣,使用 runS

避免使用 State 产生的问题。不要混淆扩展函数 arrow.data.run 与扩展函数 kotlin.run(默认导入)。

使用 State 的核心递归

State 在核心递归中很有用;我们可以用 State 重新编写我们的旧例子:

fun <T, S> unfold(s: S, f: (S) -> Pair<T, S>?): Sequence<T> {
   val result = f(s)
   return if (result != null) {
      sequenceOf(result.first) + unfold(result.second, f)
   } else {
      sequenceOf()
   }
}

我们原始的 unfold 函数使用一个函数,f: (S) -> Pair<T,S>?,这与 State<S, T> 非常相似:

fun <T, S> unfold(s: S, state: State<S, Option<T>>): Sequence<T> {
    val (actualState: S, value: Option<T>) = state.run(s)
    return value.fold(
            { sequenceOf() },
            { t ->
                sequenceOf(t) + unfold(actualState, state)
            })
}

我们不再使用 lambda (S) -> Pair<T, S>?,而是使用 State<S, Option<T>>,并使用 Option 的 fold 函数,对于 None 使用空 Sequence,对于 Some<T> 使用递归调用:

fun factorial(size: Int): Sequence<Long> {
   return sequenceOf(1L) + unfold(1L to 1) { (acc, n) ->
      if (size > n) {
         val x = n * acc
         (x) to (x to n + 1)
      } else
         null
   }
}

我们旧的阶乘函数使用 unfoldPair<Long, Int> 和 lambda—(Pair<Long, Int>) -> Pair<Long, Pair<Long, Int>>?

import arrow.syntax.option.some

fun factorial(size: Int): Sequence<Long> {
    return sequenceOf(1L) + unfold(1L toT 1, State { (acc, n) ->
        if (size > n) {
            val x = n * acc
            (x toT n + 1) toT x.some()
        } else {
            (0L toT 0) toT None
        }
    })
}

重新编写的阶乘使用 State<Tuple<Long, Int>, Option<Long>>,但内部逻辑几乎相同,尽管我们新的阶乘没有使用 null,这是一个显著的改进:

fun fib(size: Int): Sequence<Long> {
   return sequenceOf(1L) + unfold(Triple(0L, 1L, 1)) { (cur, next, n) ->
      if (size > n) {
         val x = cur + next
         (x) to Triple(next, x, n + 1)
      }
      else
         null
   }
}

同样,fib 使用 unfold 与 Triple<Long, Long, Int> 和 lambda (Triple<Long, Long, Int>) -> Pair<Long, Triple<Long, Long, Int>>?

import arrow.syntax.tuples.plus

fun fib(size: Int): Sequence<Long> {
    return sequenceOf(1L) + unfold((0L toT 1L) + 1, State { (cur, next, n) ->
        if (size > n) {
            val x = cur + next
            ((next toT x) + (n + 1)) toT x.some()
        } else {
            ((0L toT 0L) + 0) toT None
        }
    })
}

重新编写的 fib 使用 State<Tuple3<Long, Long, Int>, Option<Long>>。请注意,扩展操作符函数 plus,与 Tuple2<A, B>C 一起使用将返回 Tuple3<A, B, C>

fun main(args: Array<String>) {
    factorial(10).forEach(::println)
    fib(10).forEach(::println)
}

现在,我们可以使用我们的核心递归函数来生成序列。State 的其他许多用途我们在这里无法涵盖,例如来自 企业集成模式消息历史www.enterpriseintegrationpatterns.com/patterns/messaging/MessageHistory.html)或具有多个步骤的表单导航,如飞机检查或长注册表单。

摘要

Arrow 提供了许多数据类型和类型类,可以显著简化复杂任务,并提供了一套标准的惯用和表达式。在本章中,我们学习了如何使用 Option 抽象 null 值,以及如何使用 EitherTry 表达计算。我们创建了一个数据类型类,还学习了单调组合和转换。最后但同样重要的是,我们使用了 State 来表示应用程序状态。

通过本章,我们到达了这次旅程的终点,但请放心,这并不是你学习函数式编程的终点。正如我们在第一章所学,函数式编程全部关于使用函数作为构建块来创建复杂程序。同样,通过在这里学到的所有概念,你现在可以理解和掌握新的、令人兴奋的、更强大的想法。

现在你开始了一段新的学习之旅。

第十四章:Kotlin 快速入门

本书旨在为已经熟悉 Kotlin 工作方式的人编写。但如果你对这个语言完全陌生,不用担心,我们为你提供了阅读、理解和充分利用本书所需的一切。

在本附录中,我们将涵盖以下主题:

  • 使用 Kotlin 的不同方式

  • Kotlin 的基本控制结构

  • 其他资源

编写和运行 Kotlin

我们将涵盖一套全面的选项来编写和运行 Kotlin,从最简单的到最专业的。

Kotlin 在线

没有需要安装的,只需打开 Kotlin (try.kotlinlang.org/)。Kotlin 在线包含了编写和运行简单 Kotlin 程序所需的一切,包括 JVM 和 JavaScript 编译选项。如果你有账户,你甚至可以创建和保存你的程序:

在你的控制台中

对于任何严肃且打算用于生产代码的内容,在线选项并不是最佳选择。让我们来探讨如何在你的机器上安装它。

安装 SDKMAN

安装 Kotlin 最便捷的选项是使用 SDKMAN,这是一个用于安装和更新 JVM 工具的工具。

使用以下命令安装 SDKMAN(如果你还没有安装):

$ curl -s "https://get.sdkman.io" | bash

一旦 SDKMAN 安装完成,我们就可以使用它来安装 Kotlin 并保持其更新,以及其他工具,如 Gradle 和 Maven。

通过 SDKMAN 安装 Kotlin

要通过 SDKMAN 安装 Kotlin,你只需输入以下内容:

$ sdk install kotlin

现在我们已经在我们的控制台中有了 Kotlin 命令。

Kotlin 的 REPL

要与 Kotlin 的 REPL 互动,你可以输入以下内容:

$ kotlinc

现在你可以输入并执行 Kotlin 表达式:

$ kotlinc
Welcome to Kotlin version 1.2.21 (JRE 1.8.0_111-b14)
Type :help for help, :quit for quit
>>> println("Hello, World!")
Hello, World!
>>> 

要退出 Kotlin 的 REPL,你可以输入:quit

编译和执行 Kotlin 文件

支持 Kotlin 的编辑器有多个——Micro、Vim、NeoVim、Atom 和 VS Code。

我首选使用的编辑器是 Micro,(micro-editor.github.io/index.html)。它是一个快速且易于使用的编辑器(有点像超强大的 Nano):

在你喜欢的编辑器中,创建一个名为hello.kt的文件,并输入以下代码:

fun main(args: Array<String>) {
    println("Hello, World!")
}

现在在你的控制台中,使用以下命令编译你的程序:

$ kotlinc hello.kt

要执行它,请在你的控制台中输入以下内容:

$ kotlin HelloKt

使用 Gradle

对于任何超过几个文件的任何项目,构建工具都成为必需品。构建工具为你提供了一种实用方式来编译、管理库以及打包和执行应用程序。Gradle (docs.gradle.org/current/release-notes.html) 是一个支持多种语言的构建工具,包括 Kotlin。

通过 SDKMAN 安装 Gradle

要通过 SDKMAN 安装 Gradle,你只需输入以下内容:

sdk install gradle

现在我们已经在我们的控制台中有了gradle命令。

创建可分发的 Gradle 命令

虽然 Gradle 是一个伟大的工具,但共享依赖于 Gradle 构建的代码并不容易。我们开源项目的潜在用户可能没有安装 Gradle,甚至可能没有我们相同的 Gradle 版本。幸运的是,Gradle 提供了一种创建可分发 Gradle 工具或命令的方法。

在一个新、干净的目录中,在您的控制台输入以下内容:

$ gradle wrapper

此命令创建了一个可执行的 gradlew 脚本。第一次运行此命令时,它会下载所有必要的 Gradle 文件,然后成为一个可重复执行的命令。

创建 Gradle 项目文件

为了 Gradle 能够工作,我们必须有一个 build.gradle 文件。在这个文件中,我们将为 Gradle 设置不同的选项和设置,以便其使用和运行。

对于我们的基本 Hello World 程序,我们的文件必须看起来像这样:

group 'com.packtpub'
version '1.0'

buildscript {
    ext.kotlin_version = '1.2.21'

    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin'
apply plugin: 'application'

mainClassName = 'com.packtpub.appendix.HelloKt'

defaultTasks 'run'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

通常,在 build.gradle 文件中,我们定义一些插件(Gradle 插件定义了对语言和框架的支持),存储库以下载依赖项,依赖项本身,以及其他选项。

在此文件中,我们定义了两个插件——一个用于 Kotlin,另一个用于将我们的项目定义为具有起始点或主类的应用程序。

创建我们的 Hello World 代码

Gradle 默认搜索 Kotlin 文件的目录位置是 src/main/kotlin。我们将 hello.kt 文件放置在 src/main/kotlin/com/packtpub/appendix 目录中:

package com.packtpub.appendix

fun main(args: Array<String>) {
    println("Hello, World!")
}

现在我们可以使用以下命令一次性编译和运行:

$ ./gradlew

由于我们已定义默认任务为 run 且主类为 com.packtpub.appendix.HelloKt,只需运行 ./gradlew 就可以构建并运行我们的程序。

使用 IntelliJ IDEA 或 Android Studio

IntelliJ IDEA 和 Android Studio(它基于 IntelliJ IDEA 的开源版本)是 Kotlin(以及其他语言,如 Java、Scala、Groovy 等)的出色 IDE。两者都提供自动完成、格式化和对 Gradle 和 Maven 的支持。

这两个 IDE 都有很好的文档,你可以在它们的网站上了解更多关于 Kotlin 的支持信息:

使用 IntelliJ IDEA 导入 Gradle 文件

我们可以在 IntelliJ IDEA 中导入我们的 Gradle 项目。一旦启动 IDEA,我们就可以打开我们的 build.gradle 文件:

将会弹出一个对话框,询问您是否想将其作为文件或项目打开。点击“打开为项目”:

如果您已安装 JDK 并在 IntelliJ IDEA 中设置,只需点击“确定”。如果您没有配置 JDK,那么请进行配置(IntelliJ IDEA 将引导您完成配置)然后继续点击“确定”:

现在,我们可以使用 IntelliJ IDEA 的所有功能来编辑和我们的项目:

你可以用同样的方式打开这本书中的所有示例代码。

基本 Kotlin 语法

对于有 C 风格语法(如 C、Java、Scala、Groovy 和 TypeScript)经验的开发者来说,Kotlin 语法看起来很熟悉。

通用特性

各种 Kotlin 功能在 JVM 语言中都很常见。如果你有 Java 的经验,你会觉得 Kotlin 很亲切。

包是一组文件(通常定义在同一个目录中),用于定义逻辑单元,例如控制器和存储库。

要设置特定包中的文件,请在第一行使用 package 关键字:

package com.packt.functionalkotlin

理想情况下,包 com.packt.functionalkotlin 内的文件应该位于目录 com/packt/functionalkotlin 中。这使文件更容易找到,但在 Kotlin 中不是强制性的。

字符串连接和插值

Kotlin 中的字符串连接使用加号(+)运算符:

val temperature = 12

println("Current temperature: " + temperature + " Celsius degrees")

字符串插值是进行复杂连接的简单方法:

val temperature = 12

println("Current temperature: $temperature Celsius degrees")

使用符号美元($)让你可以在字符串内部使用简单值:

val temperature = 12

println("Temperature for tonight: ${temperature - 4} Celsius degrees")

对于比仅仅使用值更复杂的情况,你可以使用带有大括号的美元符号(${... }):

注释

单行注释使用双斜杠(//):

// This is a single line comment

println("Hello, World!") // This is a single line comment too, after valid code

块注释使用斜杠和星号来打开块(/*)以及星号和斜杠来关闭(*/):

/*
This is a multi-line comment,
Roses are red
... and I forgot the rest

*/

println(/*block comments can be inside valid code*/ "Hello, World!")

控制结构

Kotlin 有四种基本的控制结构——ifwhenforwhile

if 表达式

Kotlin 中的 if 看起来与任何其他 C 风格语言完全一样:

if (2 > 1) { //Boolean expression
    println("2 is greater than 1")
} else {
    println("This never gonna happen")
}

在 Kotlin 中,if(以及 when)是一个表达式。这意味着 if 语句会返回一个值:

val message = if (2 > 1) {
    "2 is greater than 1"
} else {
    "This never gonna happen"
}

println(message)

Kotlin 没有三元表达式,但可以用单行写一个 if 表达式:

println(if(2 > 1) "2 is greater than 1" else "This never gonna happen")

when 表达式

与其他 C 风格语言不同,Kotlin 没有使用 switch 语句,但有一个更加灵活的 when 表达式:

val x: Int = /*Some unknown value here*/

when (x) {
    0 -> println("x is zero")
    1, 2 -> println("x is 1 or 2")
    in 3..5 -> println("x is between 3 and 5")
    else -> println("x is bigger than 5... or maybe is negative...")
}

when 是表达式:

val message = when {
    2 > 1 -> "2 is greater than 1"
    else -> "This never gonna happen"
}

println(message)

它们也可以用来替换 if 表达式:

for 循环

for 循环可以遍历任何提供迭代器的对象(例如,集合和范围):

for(i in 1..10) { // range
    println("i = $i")
}

while 和 do 循环

whiledo 循环是标准的 C 风格循环:

var i = 1

while (i <= 10) {
    println("i = $i")
    i++
}

do {
    i--
    println("i = $i")
} while (i > 0)

现在你已经拥有了所有基本的部分,你就有了一切阅读和理解这本书内容所需的东西。

进一步学习

如果你想要提升你的 Kotlin 知识和理解,前进的最佳方式是尝试 Kotlin Koans。

Kotlin Koans 是一个教程,它将引导你在几天内从初学者逐步成为一名非常熟练的 Kotlin 程序员,而且更好的是,它是免费的。

你可以在 try.kotlinlang.org/#/Kotlin%20Koans/Introduction/Hello,%20world!/Task.kt 尝试 Kotlin Koans:

posted @ 2025-10-09 13:23  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报