vlambda博客
学习文章列表

读书笔记《functional-kotlin》函数式、应用式和单元式

Chapter 10. Functors, Applicatives, and Monads

Functors、applicatives 和 monads 是与函数式编程相关的搜索最多的词之一,如果您认为没有人知道它们的意思,这是有道理的(不是真的,有聪明的人知道他们在说什么)。尤其是关于 monad 的困惑已经成为编程社区中的一个笑话/模因:

"monad 是内函子范畴中的幺半群,有什么问题?"

James Iry 在他的经典博文A Brief, Incomplete and Mostly Wrong History of Programming Languages 中虚构地归因于 Philip Wadler 的这句话,(< a class="ulink" href="http://james-iry.blogspot.co.uk/2009/05/brief-incomplete-and-mostly-wrong.html" target="_blank">http://james -iry.blogspot.co.uk/2009/05/brief-incomplete-and-mostly-wrong.html)。

在本章中,我们将介绍以下主题:

  • Functors
  • Options, lists, and functions as functors
  • 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< /code> 类别到 String 类别。同样的道理,在我们的例子中,我们从 List<Int> 转换为 List<Int> (不是那个令人兴奋的),然后从 List<Int>List<String>。我们没有改变外部类型,只改变了内部值。

那是一个函子。 functor 是一种定义转换或映射其内容的方法的类型。您可以找到函子的不同定义,或多或少是学术性的;但原则上,所有这些都指向同一个方向。

让我们为仿函数类型定义一个通用接口:

interface Functor<C<_>> { //Invalid Kotlin code
    fun <A,B> map(ca: C<A>, transform: (A) -> B): C<B>
}

而且,它不能编译,因为 Kotlin 不支持更高种类的类型。

Note

您可以在 第 13 章箭头类型。 >

支持更高种类的类型的语言中,例如ScalaHaskell,是可能 来定义一个 Functor 类型,例如,Scala 猫函子:

trait Functor[F[_]] extends Invariant[F] { self =>
  def map[A, B](fa: F[A])(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 value 的行为将 不同 用于 一些无:

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

我们在这里改变的是返回类型从 BC 通过应用参数函数 变换:(B)-> C 到函数 (A) -> 的结果B 本身:

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
}

如果您有其他函数式编程语言的经验,请将此行为视为前向函数组合(第 12 章Arrow 入门)。

Monads


monadfunctor 类型定义了一个 flatMap(或 bind,在其他语言中)函数,该函数接收返回相同类型的 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 value 的内容,但在  flatMap,我们可以返回一个新的 List type 包含更少或更多的项目,使其比 map 更有效

所以,一个通用的 monad 看起来像这样(请记住,我们没有更高种类的类型):

interface Monad<C<_>>: Functor<C> { //Invalid Kotlin code
    fun <A, B> flatMap(ca:C<A>, fm:(A) -> C<B>): C<B>
}

现在,我们可以编写一个 flatMap 函数 for 我们的 选项类型:

fun <T, R> Option<T>.flatMap(fm: (T) -> Option<R>): Option<R> = when (this) {
    Option.None -> Option.None
    is Option.Some -> fm(value)
}

如果你仔细观察,你会发现 flatMap 和 map 看起来非常相似;非常相似,我们可以使用 flatMap 重写 map

fun <T, R> Option<T>.map(transform: (T) -> R): Option<R> = flatMap { t -> Option.Some(transform(t)) }

现在我们可以以很酷的方式使用 flatMap 函数的强大功能,而这对于普通地图来说是不可能的:

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,我们返回 5.0 的折扣包裹在 Some< /code>,如果不是,则 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 函数中,我们对这两个值都有 access并对它们进行操作。

我们可以通过结合 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 操作(更多关于理解在 第 13 章箭头类型)。

Applicatives


我们之前的示例,在包装器中调用 lambda,并在同种包装器中使用参数,这是引入应用程序的完美方式。

applicative 是一种 定义的类型两个函数,一个 pure(t: T) 函数返回  T 值 包装在应用类型中,以及一个ap 函数 (apply,在其他语言中)接收包装在 applicative 类型中的 lambda。

在上一节中,当我们解释 monad 时,我们使它们直接从 functor 扩展,但实际上,monad 从 applicative 扩展,applicative 从 functor 扩展。因此,我们的通用应用程序的伪代码以及整个层次结构将如下所示:

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 是更强大的 functor,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
}

更容易阅读,但需要注意的是——结果的顺序不同。我们需要了解并选择适合我们特定情况的选项。

我们可以将 pureap 添加到我们的 Option 类中:

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,这正是我们组合一元操作的方式。

对于 monad,我们使用 summed 两个 Option<Int> ="literal">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)
}

现在,使用应用程序:

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

这不是很容易阅读。首先,我们将 maybeFive 映射到一个 lambda (Int) -> (整数)-> Int(技术上是一个柯里化函数,关于柯里化函数的更多信息在 第 12 章Arrow 入门),返回一个Option< (整数)-> 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 运算;多么酷啊?现在,让我们看看下面的代码,使用 applicatives 对两个 Option<Int> 求和

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

我们将 (Int) -> (整数)-> Int lambda 用pure函数然后我们应用 Option , 一一.我们使用名称 `(*)` 作为对 Haskell 的 <*> 的致敬。

到目前为止,您可以看到 applicatives 可以让您做一些 很酷的技巧,但 monad 更强大、更灵活。什么时候使用其中一种?这显然取决于您的特定问题,但我们的一般建议是尽可能少地使用抽象。你可以从 functor 的 map 开始,然后是 applicative 的 ap,最后是 monad 的 flatMap 。一切都可以用 flatMap (如你所见  Option, map< /code> 和 ap 是使用 flatMap) 实现的,但大多数时候是 map ap 可以更容易理解。

回到函数,我们可以让函数表现为应用程序。首先,我们应该添加一个纯函数:

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 入门)。

让我们将 flatMap,一个 ap,添加到函数 (A) -> B

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

访问函数组合中的原始参数在多种情况下很有用,例如调试和审计。

Summary


我们已经介绍了很多很酷的概念,它们的名字很吓人,但背后却有着简单的想法。函子、应用程序和单子类型 为我们将在接下来的章节中介绍的几个抽象和更强大的函数概念打开了大门。我们了解了 Kotlin 的一些限制,以及我们如何在创建函数来模拟不同类型的函子、应用程序和单子时克服这些限制。我们还探讨了函子、应用程序和单子之间的层次关系。

在下一章中,我们将介绍 如何有效地处理数据流。