vlambda博客
学习文章列表

读书笔记《functional-kotlin》有关函数的更多信息

Chapter 5. More on Functions

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

一些功能,例如 Domain Specific Languages (DSLs),让开发人员语言扩展到最初设计Kotlin时未考虑的领域。

在本章结束时,您将全面了解所有功能特性,包括:

  • Extension functions
  • Operator overloading
  • Type-safe builders
  • Inline functions
  • Recursion and corecursion

Single-expression functions


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

函数 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

Note

当您试图返回的类型非常明显时,使用类型推断来返回函数。一个好的经验法则是将它用于简单类型,例如数值、布尔值、字符串和简单的数据类 构造函数。任何更复杂的东西,特别是如果函数进行任何转换,都应该有明确的类型。你未来的自己会很高兴!

Parameters


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

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

}

每个参数定义为parameterName: ParameterType,在我们的例子中,name: String大小:整数。这里没有什么新鲜事。

vararg

parameters 有两种我们已经 涵盖——vararg 和 lambdas:

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

我们已经讨论过,如果函数的最后一个 parameter 是 lambda,那么它不能在 括号外传递 和花括号内,好像 lambda 本身就是一个控制结构体。

我们在 unless 函数">第 2 章函数式编程入门,在第一节-类和高阶函数。让我们看一下下面的代码:

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() }

让我们来点冒险,一个 vararg lambdas 参数:

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)})

Named parameters

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

functions 有很多参数有什么问题?

  • They are hard to use. This can be alleviated or fixed with default parameters that we will cover in the next section, Default parameters.
  • They are hard to read—named parameters to the rescue.
  • They are probably doing too much. Are you sure that your function isn't too big? Try to refactor it and clean up. Look for possible side effects and other harmful practices. A special case is data class constructors, as they are just autogenerated assignments.

使用命名参数,您可以为任何函数调用添加可读性。

让我们以 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)

Named parameters on high-order functions

通常,当我们定义一个高阶函数时,我们从不name 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 会产生编译错误。

Default parameters

在 Kotlin 中,函数 parameters 可以有默认值。对于 ProgrammerfavouriteLanguage 和 yearsOfExperience 数据类 具有默认值(请记住,构造函数也是函数):

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)

Extension functions


毫无疑问,Kotlin 的最佳特性之一是 extension 函数。扩展 functions 让您可以使用新函数修改现有类型:

fun String.sendToConsole() = println(this)

fun main(args: Array<String>) {
   "Hello world! (from an extension function)".sendToConsole()
}

要将扩展函数添加到现有类型,您必须在类型名称旁边写下函数名称,并用点 (.) 连接。

在我们的示例中,我们将扩展函数 (sendToConsole()) 添加到 String 类型。在函数体内,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),按名称调用函数。  ;

Extension functions and inheritance

当我们谈论继承时,成员函数和 extension 函数有很大的不同。

开放类 Canine 有一个子类Dog。一个独立的函数, printSpeak,接收 Canine 类型的参数并打印函数 speak(): 字符串:

open class Canine {
   open fun speak() = "<generic canine noise>"
}

class Dog : Canine() {
   override fun speak() = "woof!!"
}

fun printSpeak(canine: Canine) {
   println(canine.speak())
}

我们已经在 第 1 章Kotlin – 数据类型、对象和类,在 继承 部分。带有 open 方法(成员函数)的开放类可以扩展并改变它们的行为。调用 speak 函数 会根据您的实例类型而有所不同。

printSpeak 函数可以被任何is-a< code class="literal">Canine,Canine 本身或任何子类:

printSpeak(Canine())
printSpeak(Dog())

如果我们执行这段代码,我们可以在控制台上看到:

读书笔记《functional-kotlin》有关函数的更多信息

虽然两者都是 Canine,但 speak 的行为在这两种情况下是不同的,因为子类覆盖了父实现。

但是使用 extension 函数,很多东西都不同了。

与前面的示例一样, 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())
}

扩展函数不需要标记为覆盖,因为我们没有覆盖任何东西:

printSpeak(Feline())
printSpeak(Cat()

如果我们执行这段代码,我们可以在控制台上看到:

读书笔记《functional-kotlin》有关函数的更多信息

在这种情况下,两个调用都会产生相同的结果。虽然一开始看起来很混乱,但一旦你分析了正在发生的事情,它就会变得清晰。我们调用了 Feline.speak() 函数 两次;这是 因为我们传递的每个参数都是 FelineprintSpeak(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"))

如果我们执行这段代码,我们可以在控制台上看到:

读书笔记《functional-kotlin》有关函数的更多信息

 

在这种情况下,它的行为仍然与前面的示例相同,但为 name 使用了正确的值。说到这里,我们可以用 namethis.name 来引用 name ;两者都是有效的。

Extension functions as members

扩展函数可以声明为类的成员。声明了 extension 函数的类的实例称为 调度接收器.

 Caregiver 内部开放类 定义了 extension 两个不同的函数类,FelinePrimate

open class Caregiver(val name: String) {
   open fun Feline.react() = "PURRR!!!"

   fun Primate.react() = "*$name plays with ${[email protected]}*"

   fun takeCare(feline: Feline) {
      println("Feline reacts: ${feline.react()}")
   }

   fun takeCare(primate: Primate){
      println("Primate reacts: ${primate.react()}")
   }
}

这两个扩展函数都打算在 Caregiver 的实例中使用。实际上,如果成员扩展函数未打开,则将其标记为私有是一种很好的做法。

对于 Primate.react(),我们使用 Primate 的 nameCaregiver 中的 name 值。要访问具有名称冲突的成员,扩展接收器 (this) 具有优先权,并且要访问调度程序接收器的成员,合格的 this 语法。没有名称冲突的调度程序接收器的其他成员可以在没有限定的 this 的情况下使用。

不要对我们已经介绍过的 this 的各种方式感到困惑:

  • Inside a class, this means the instance of that class
  • Inside an extension function, this means the instance of the receiver type like the first parameter in our utility function with nice syntax:
class Dispatcher {
   val dispatcher: Dispatcher = this

   fun Int.extension(){
      val receiver: Int = this
      val dispatcher: Dispatcher = this@Dispatcher
   }
}

回到我们的 Zoo 示例, 我们实例化一个 Caregiver,一个 Cat 和一个 Primate,我们用两个动物实例调用函数 Caregiver.takeCare

val adam = Caregiver("Adam")

val fulgencio = Cat()

val koko = Primate("Koko")

adam.takeCare(fulgencio)
adam.takeCare(koko)

如果我们执行这段代码,我们可以在控制台上看到:

读书笔记《functional-kotlin》有关函数的更多信息

任何动物园都需要兽医。 Vet 类扩展了 Caregiver

open class Vet(name: String): Caregiver(name) {
   override fun Feline.react() = "*runs away from $name*"
}

我们用 different 实现覆盖了 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)
}

之后,我们得到以下输出:

读书笔记《functional-kotlin》有关函数的更多信息

Extension functions with conflicting names

extension 函数与成员函数同名时会发生什么?

Worker 类有一个函数 work(): String 和一个私有函数 rest() :字符串。我们还有两个具有相同签名的扩展函数,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*"

具有相同签名的扩展函数不是编译错误,而是一个警告:Extension is shadowed by a member: public final fun work(): String

声明与成员函数具有相同签名的函数是合法的,但成员函数始终具有优先权,因此永远不会调用扩展函数。当成员函数为私有时,此行为会发生变化,在这种情况下,扩展函数优先。

也可以使用扩展函数重载现有的成员函数:

val worker = Worker()

println(worker.work())

println(worker.work("refactoring"))

println(worker.rest())

在执行时, work() 调用成员函数和 work(String) 和 rest() 是扩展函数:

读书笔记《functional-kotlin》有关函数的更多信息

Extension functions for objects

在 Kotlin 中,对象是一种类型,因此它们可以具有函数,包括 extension 函数(其中包括扩展接口和别的)。

我们可以给对象添加一个buildBridge扩展函数, Builder

object Builder {

}

fun Builder.buildBridge() = "A shinny new bridge"

我们可以包含伴随对象。 类 Designer 有两个内部对象,companion 对象和 桌面 对象:

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)

Infix functions


只有一个参数的函数(普通或扩展)可以被标记 作为 infix 并与 infix 表示法一起使用。 infix 表示法对于自然地表达某些领域的代码很有用,例如数学和代数运算。

让我们为 Int 类型添加一个infix 扩展函数,superOperation (这只是一个带有花哨名称的常规总和):

infix fun Int.superOperation(i: Int) = this + i

fun main(args: Array<String>) {
   1 superOperation 2
   1.superOperation(2)
}

我们可以将 superOperation函数与中缀表示法或普通表示法一起使用。

infix 符号常用的另一个领域是 assertion 库,例如 HamKrest (https://github.com/npryce/hamkrest) 或 Kluent (https://github.com/MarkusAmshove/Kluent)。用自然、易于理解的语言编写规范代码是一个巨大的优势。

Kluent 断言看起来像自然的英语表达:

"Kotlin" shouldStartWith "Ko"

Kluent 还附带了一个反引号版本,以提高可读性:

"Kotlin" `should start with` "Ko"

反引号 (`) 让您可以编写任意标识符,包括在 Kotlin 中保留的单词。现在,您可以编写自己的颜文字功能了:

读书笔记《functional-kotlin》有关函数的更多信息

您可以链接许多 infix 函数来生成内部 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  function, receives Pair<Base, Us> 作为参数(一种Kotlin 标准库中广泛使用的元组)和 infix 扩展函数 K.to(v: V) 返回一个 Pair 使用接收者作为第一个成员,参数作为第二个参数(to 可以使用任何类型的组合来调用)。

Operator overloading


运算符重载是多态的一种形式。一些 operators 会改变不同类型的行为。经典的例子是运算符加号 (+)。在数值上,加号是求和运算,在 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("..."))
}

运算符函数 plus 返回一个 Pack 值。要调用它,可以使用 infix 运算符方式(Wolf + Wolf)或普通方式(Wolf.plus(Wolf))。

关于 Kotlin 中的运算符重载需要注意的一点——您可以在 Kotlin 中覆盖的运算符是有限的;您不能创建任意运算符。

Binary operators

二元运算符receive一个参数(这个规则有例外——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)

y 中的 x

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 <是的

x.compareTo(y) < 0

必须返回 Int

x >是的

x.compareTo(y) > 0

必须返回 Int

x <= y

x.compareTo(y) <= 0

必须返回 Int

x >= y

x.compareTo(y) >= 0

必须返回 Int

Invoke

回到 第 2 章函数式编程入门,在 一等函数和高阶函数部分,  当我们介绍 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 函数是一个运算符,一个 curious 之一。 invoke 运算符可以在没有 调用 >名称

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)

Indexed access

indexed 访问操作符是数组读写operations 带方括号 ([]),用于具有类 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..., yN]

x.get(y1, y2..., yN)

 

 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

Note

运算符 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)

返回值被忽略

Unary operators

一元运算符没有参数并直接在调度程序中执行。

我们可以在 Wolf 类中添加 not 运算符:

operator fun Wolf.not() = "$name is angry!!!"

!talbot // talbot.not()

下表将向您展示所有可能的一元 operators 可以重载:

操作员

等效

备注

+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 的调用,应该返回与调度程序类型兼容的类型,不应该改变调度程序。

 

后缀(增量和减量)返回原始值,然后使用运算符返回值更改变量。 Prefix 返回运算符的返回值,然后使用该值更改变量。

Type-safe builders


通过前面两节(infix 函数和运算符重载),我们为构建出色的 DSL 奠定了良好的基础。 DSL 是一种专业化特定的 域,与通用语言  (GPL)。 DSL 的经典示例(即使人们没有意识到)是 HTML(标记)和 SQL(关系数据库查询)。  

Kotlin 提供了许多功能来创建内部 DSL(一种在主机 GPL 内部运行的 DSL),但是我们仍然需要介绍一个功能,即类型安全构建器。类型安全构建器让我们以(半)声明方式定义数据,并且对于定义 GUI 非常有用,  HTML 标记、XML 等。

TornadoFX 是一个漂亮的 Kotlin DSL 示例。 TornadoFX (https:// tornadofx.io/) 是用于创建 JavaFX 应用程序的 DSL。

我们编写一个 FxApp 类 它扩展了 tornadofx.App 并接收一个tornadofx。查看类(类引用,而不是实例):

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 应用程序:

读书笔记《functional-kotlin》有关函数的更多信息

当然,现在它什么也没做,但是如果你将它与 Java 进行比较,使用 TornadoFX 创建一个 JavaFX 应用程序很简单。有 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 的文档中,这种函数被命名为 function type with receiver and with any instance of T,< /code> 我们可以调用 block 函数。不用担心 inline 修饰符,我们将在下一节中介绍它。

Note

使用接收器理解函数类型的一个技巧是将其视为扩展函数。看看带有那个熟悉的点 (.) 的声明,在函数内部,我们可以通过 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 用于 initializing实例

public inline fun <T> T.apply(block: T.() -> Unit): T {    
    block()
    return this
}

如您所见,所有这些函数都非常易于理解,但它们大大提高了 Kotlin 的实用性和可读性。

Creating a DSL

我最大的爱好之一是骑自行车。 emotion 运动、努力、健康益处和享受风景是其中的一些好处(我可以保持持续不断)。

我想创建一种方法来注册我的自行车及其组件。对于原型阶段,我将使用 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 和一个 init 函数与接收者 T.() ->单位 。在内部,执行init 函数并将元素添加为子元素。

Part 使用 @ElementMarker 注释进行注释,该注释本身也使用 @DslMarker 。它防止内部元素到达外部元素。

在这个例子中,我们可以使用 frame

val commuter = bicycle {
   description("Fast carbon commuter")
   bar {
      barType = FLAT
      material = ALUMINIUM
      frame {  } //compilation error
   }

仍然可以使用 this qualified 明确地做到这一点:

val commuter = bicycle {
   description("Fast carbon commuter")
   bar {
      barType = FLAT
      material = ALUMINIUM
      [email protected]{ }
   }

现在,列举几个描述材料、杆类型和制动器的例子:

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< /code> 映射,来回转换值:

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 函数和 frame 的函数,forkbar。每个函数接收一个 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 有一个 property 用于它的类型,使用 < code class="literal">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 中的 DSL 具有 infix 函数、运算符重载和类型安全的构建器非常强大,而且 Kotlin 社区每天都在创建新的和令人兴奋的库。

Inline functions


高阶函数非常 有用且花哨,但它们带有一个警告——性能损失。请记住,来自 第 2 章函数式编程入门,在一等函数和高阶函数,在编译时,一个 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 函数更快,尽管生成更多字节码:

读书笔记《functional-kotlin》有关函数的更多信息

每次执行 2.3 毫秒看起来并不多,但从长远来看,通过更多优化,可以产生明显的复合效果。

Inline restrictions

内联 lambda 函数有一个 important 限制——它们不能以任何方式(存储、复制等)进行操作。

UserService 存储了一个监听器列表 (User) ->单位

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 函数会产生编译错误:

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
}

inline 函数上使用 noinline 只会内联高阶函数体,而不是 noinline lambda 参数(inline 高阶函数可以同时具有:inlinenoinline lambdas)。生成的字节码不如完全内联函数快,编译器会显示警告。

内联 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)
}

生成的代码相当复杂。生成了许多片段:

  • A class that extends (String) -> User to represent buildUser and internally creates User using String::toLowerCase to transform the name
  • A normal inline code to execute List<User>.map() using an instance of the class that represents buildUser
  • List<T>.map() is inline, so that code gets generated too

一旦你意识到它的限制,内联高阶函数是提高代码执行速度的好方法。事实上,Kotlin 标准库中的许多高阶函数都是内联

Recursion and corecursion


在 第 2 章中,开始使用函数式编程,在递归部分,我们广泛地介绍了递归(尽管有是本书范围之外的递归主题)。

我们使用 recursion 来编写斐波那契等经典算法 (我们正在重用 tailrecFib< /code> 来自 第 2 章函数式编程入门):

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 ="ch02">第 2 章函数式编程入门):

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 只是使用 < code class="literal">fold 有不同的变体。

因此,一方面,递归采用复杂值并将其简化为所需的答案,另一方面,corecursion 采用一个值并在其之上构建以产生一个复合值(包括可能无限的数据结构,例如 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 采用 S 步骤并生成 Pair<T, S>? (a nullable Pair) T 值 添加到序列和下一个 S 步骤。 

如果 f(s) 的结果为空,我们返回一个空序列,否则我们创建一个单值序列并添加 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,它采用当前步骤并将其与 numOfValues,返回 Pair<T, Int> 与相同元素和当前步+ 1 null。

没关系,但不是很有趣。返回阶乘序列怎么样?我们为您服务:

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 (第一个元素进行计算,第二个元素根据大小进行评估),因此,我们的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>

生成 Factorial 和 Fibonacci 序列的核心递归实现是分别计算 Factorial 或 Fibonacci 数的递归实现的镜像——有些人认为这更容易理解。

Summary


在本章中,我们已经介绍了函数式编程的大部分 Kotlin 特性。我们回顾了如何使用单表达式函数编写更短的函数、不同类型的参数、如何使用扩展函数扩展我们的类型,以及如何使用 infix 编写自然且可读的代码函数和运算符。我们还介绍了使用类型安全构建器进行 DSL 创作的基础知识,以及如何编写高效的高阶函数。最后但同样重要的是,我们了解了递归和核心递归。

在下一章中,我们将了解 Kotlin 代表。