vlambda博客
学习文章列表

读书笔记《functional-kotlin》Arrow快速入门

Chapter 12. Getting Started with Arrow

箭头 (http: //arrow-kt.io/) 是一个 Kotlin library 提供 functional 构造、数据类型和其他抽象。 Kotlin 语法强大而灵活,Arrow 利用它来提供非标准的功能。

Arrow 是将两个最成功和最流行的函数库 funKTionaleKategory 合二为一的结果。 2017 年底,两个开发者团体担心分裂会损害整个 Kotlin 社区,决定联手创建一个单一的、统一的功能库。

在本章中,我们将介绍如何使用现有函数来构建新的和更丰富的函数。我们将讨论的一些主题如下:

  • Function composition
  • Partial application
  • Currying
  • Memoization
  • Pipes
  • Optics

Function composition


functional 编程的一个重要部分是concept 是以与我们使用任何其他类型相同的方式使用函数——作为值、参数、返回等。我们可以对其他类型做的一件事是将它们作为构建块来构建其他类型;相同的概念可以应用于函数。

函数组合是一种使用 existingfunctions 的技术"indexterm"> 函数;类似于 Unix 管道或通道管道,函数的结果值用作下一个函数的参数。

在 Arrow 中,函数组合是一组 infix 扩展函数:

功能

说明

撰写

将调用右侧函数的结果作为左侧函数的参数。

forwardCompose

将调用左侧函数的结果作为右侧函数的参数。

然后

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) ->字符串。换句话说,divStrong 等价于以下代码片段:

val divStrong: (String) -> String = { body -> "<div><strong>$body</div></strong>"}

对于 spanP:(String) -> String,我们组成 span:(String) -> (String)p:(String) ->字符串 如下:

val spanP: (String) -> String = { body -> "<span><p>$body</p></span>"}

请注意,我们使用相同的类型 (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 函数的行为非常复杂,但它是使用其他函数作为构建块构建的。

Partial application


通过函数组合,我们用两个 functions 创建第三个函数;使用 partial 应用程序,我们通过将参数传递给现有函数来创建新函数。

Arrow 带有两种部分应用——显式和隐式。

显式风格使用了一系列扩展函数,称为 partially1partially2,一直到部分22。隐式样式采用一系列扩展,重载 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 ?, (PickingOrder) -> Unit, (Bill) -> Unit)  函数不能用于合成。然后,我们部分应用了两个函数——一个 lambda 和一个引用。

Binding

partial 应用的一个特例是 binding。通过绑定,您将 T 参数 传递给 (T) -> R 函数但不执行它,有效地返回一个 () -> 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 的别名,但为它单独命名并使其在语义上更正确是有意义的。

Reverse


Reverse 接受任何 函数 并返回它其参数顺序相反(在其他语言中,此 function 称为 <强>翻转)。我们来看下面的代码:

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 函数可应用于从 parameters 122。

Pipes


一个 管道函数 接受一个 T 值 和invokes(T) -> R 函数 with 它:

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

pipe 类似于 function 组合,但不是生成 新函数,我们可以链接函数调用以生成新值,从而减少嵌套调用。管道是已知other< /a> 语言,例如 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< /code>,它的行为类似于 partially1

Currying


将柯里化应用于 nfunction参数,例如, (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() 可以将柯里化形式上的函数转换为普通的多参数形式。

Differences between the currying and partial application

currying 和部分应用之间存在一些混淆。一些 作者 将它们视为同义词,但它们是不同的:

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   
}

差异很大,它们可以帮助我们决定何时使用其中一个:

柯里化

部分申请

返回值

当一个 arity N 的函数被柯里化时,它会返回一个 N 大小,(咖喱形式)。

当一个函数或 arity N 被部分应用时,它返回一个 arity N - 1< /em>

参数应用

柯里化后,只能应用链的第一个参数。

任何参数都可以按任何顺序应用。

正在恢复

可以在柯里化形式上获取一个函数并将其还原为多参数函数。

由于部分应用不改变函数形式,因此不适用还原。

 

部分应用程序可以更灵活,但一些函数式风格倾向于柯里化。要掌握的重要一点是,两种样式都不同,并且都受 Arrow 支持。

Logical complement


逻辑补语可以接受任何谓词 (具有返回 Boolean type) 的函数并将其取反。我们来看下面的代码:

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) -> 的别名。布尔。从 022 参数的谓词有补充扩展函数。

Memoization


Memoization是一种技术缓存结果的纯函数。记忆函数的行为与普通函数一样,但存储与提供的参数相关联的先前计算的结果以产生该结果。

记忆化的经典例子是斐波那契:

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

}

Memoized 函数在内部使用线程安全结构来存储其结果,因此可以安全地用于协程或任何其他并发代码。

使用记忆函数有潜在的缺点。首先是读取内部缓存的过程高于实际的计算或内存消耗,因为目前,记忆函数没有公开任何行为来控制其内部存储。

Partial functions


部分函数(不要与部分应用函数混淆)是一个没有为其参数类型的每个可能值定义的函数。相比之下,total functionfunction 为每个可能的值定义。

让我们看一下以下的例子:

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? 值,但它无法处理空值。如果您尝试运行此代码,它将抛出 NullPointerException (NPE)。

Arrow 为 (T) -> 类型的部分函数提供显式类型 PartialFunction 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,因此它可以用作普通函数。

在此示例中,代码仍会引发异常,但现在类型为 IllegalArgumentException (IAE) , 带有信息丰富的消息。

为避免出现异常,我们必须将我们的部分函数转换 为一个总函数:

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) 函数我们可以重用内部谓词,在这种情况下,为fizzBu​​zz。在 orElse 链中使用时,声明顺序优先,为值定义的第一个部分函数将被执行,而链下的其他函数将被忽略.

Identity and constant


身份和 constant直截了当< /a> 函数。  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 重写我们的 fizzBu​​zz 值:

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。

Optics


Optics 是更新 immutable 数据的抽象结构优雅。一种光学形式是 Lens(或镜头,取决于库的实现)。 A 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 values:

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) -> A 设置:(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 可以为您生成这些镜头。

Configuring Arrows code generation

在 Gradle 项目中,添加一个 file 名为 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'
    ...     
}

配置完成后,您可以使用正常的构建命令生成箭头代码, ./gradlew build

Generating lenses

配置 Arrow 的代码生成后,您可以将 @lenses 注释添加到数据 classes 您希望为以下对象生成镜头:

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 并在同一个包中,因此不需要额外的导入。

Summary


在本章中,我们介绍了 Arrow 的许多特性,这些特性为我们提供了创建、生成和丰富现有函数的工具。我们使用现有的功能组成了新功能;我们包括部分应用和柯里化。我们还缓存了带有记忆的纯函数的结果,并使用镜头修改了数据结构。

Arrow 的特性开辟了使用基本功能原则创建丰富且可维护的应用程序的可能性。

在下一章中,我们将介绍更多 Arrow 功能,包括 OptionEither试试。