读书笔记《functional-kotlin》函数式、应用式和单元式
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
如果我告诉你你已经在 Kotlin 中使用函子怎么办?惊讶吗?我们来看看下面的代码:
List<T>
类有一个功能, map(transform: (T) -> R): List<R>< /代码>.
map
这个名字从何而来?它来自范畴论。当我们从 Int
转换为 String
时,我们所做的是从 Int< /code> 类别到
String
类别。同样的道理,在我们的例子中,我们从 List<Int>
转换为 List<Int>
(不是那个令人兴奋的),然后从 List<Int>
到 List<String>
。我们没有改变外部类型,只改变了内部值。
那是一个函子。 functor 是一种定义转换或映射其内容的方法的类型。您可以找到函子的不同定义,或多或少是学术性的;但原则上,所有这些都指向同一个方向。
让我们为仿函数类型定义一个通用接口:
而且,它不能编译,因为 Kotlin 不支持更高种类的类型。
Note
您可以在 第 13 章,箭头类型。 >
在 支持更高种类的类型的语言中,例如Scala和Haskell,是可能 来定义一个 Functor
类型,例如,Scala 猫函子:
在 Kotlin 中,我们没有这些特性,但我们可以按照惯例模拟它们。如果一个类型有一个函数或一个扩展函数,那么 map
是一个函子(这被称为结构类型 ,通过其结构而不是其层次结构来定义类型)。
我们可以有一个简单的 Option
类型:
然后,您可以为它定义一个 map
函数:
并以下列方式使用它:
现在,Option
value 的行为将 不同 用于 一些
和无:
扩展函数非常灵活,我们可以为函数类型编写 map
函数, (A) ->因此,B
将函数转换为函子:
我们在这里改变的是返回类型从 B
到 C
通过应用参数函数 变换:(B)-> C
到函数 (A) -> 的结果B
本身:
如果您有其他函数式编程语言的经验,请将此行为视为前向函数组合(第 12 章,Arrow 入门)。
monad 是 functor 类型定义了一个 flatMap
(或 bind
,在其他语言中)函数,该函数接收返回相同类型的 lambda。让我用一个例子来解释它。幸运的是,List<T>
定义了一个 flatMap
函数:
在 map
函数中,我们只是转换 List
value 的内容,但在 flatMap
,我们可以返回一个新的 List
type 包含更少或更多的项目,使其比 map 更有效
。
所以,一个通用的 monad 看起来像这样(请记住,我们没有更高种类的类型):
现在,我们可以编写一个 flatMap
函数 for 我们的 选项
类型:
如果你仔细观察,你会发现 flatMap
和 map 看起来非常相似;非常相似,我们可以使用 flatMap
重写 map
:
现在我们可以以很酷的方式使用 flatMap
函数的强大功能,而这对于普通地图来说是不可能的:
我们的函数calculateDiscount
接收并返回Option<Double>
。如果价格高于 50.0
,我们返回 5.0
的折扣包裹在 Some< /code>,如果不是,则
None
。
flatMap
的一个很酷的技巧是它可以嵌套:
在内部 flatMap
函数中,我们对这两个值都有 access并对它们进行操作。
我们可以通过结合 flatMap
和 map
以更短的方式编写这个示例:
因此,我们可以将我们的第一个 flatMap
示例重写为两个列表的组合——一个是数字,另一个是函数:
这种嵌套多个 flatMap
或 flatMap
与 map
组合的技术非常强大的,是另一个名为 monadic comprehensions 的概念背后的主要思想,它允许我们组合 monadic 操作(更多关于理解在 第 13 章,箭头类型)。
我们之前的示例,在包装器中调用 lambda,并在同种包装器中使用参数,这是引入应用程序的完美方式。
applicative 是一种 定义的类型两个函数,一个 pure(t: T)
函数返回 T
值 包装在应用类型中,以及一个ap
函数 (apply
,在其他语言中)接收包装在 applicative 类型中的 lambda。
在上一节中,当我们解释 monad 时,我们使它们直接从 functor 扩展,但实际上,monad 从 applicative 扩展,applicative 从 functor 扩展。因此,我们的通用应用程序的伪代码以及整个层次结构将如下所示:
简而言之,applicative 是更强大的 functor,monad 是更强大的 applicative。
现在,让我们为 List<T>
编写一个 ap
扩展函数:
让我们用 ap
函数重写它:
更容易阅读,但需要注意的是——结果的顺序不同。我们需要了解并选择适合我们特定情况的选项。
我们可以将 pure
和 ap
添加到我们的 Option
类中:
Option.pure
只是 Option.Some
构造函数的简单别名。
我们的 Option.ap
函数很吸引人:
Option.ap
和 List.ap
具有相同的主体,使用 flatMap
和 map
,这正是我们组合一元操作的方式。
对于 monad,我们使用 summed 两个
和 Option<Int>
="literal">flatMapmap:
现在,使用应用程序:
这不是很容易阅读。首先,我们将 maybeFive
映射到一个 lambda (Int) -> (整数)-> Int
(技术上是一个柯里化函数,关于柯里化函数的更多信息在 第 12 章,Arrow 入门),返回一个Option< (整数)-> Int>
可以作为 maybeTwo.ap
的参数传递。
我们可以用一个小技巧(我从 Haskell 借来的)让事情变得更容易阅读:
infix
扩展函数 Option<(T) -> R>.`(*)`
会让我们从左到右读取 sum
运算;多么酷啊?现在,让我们看看下面的代码,使用 applicatives 对两个 Option<Int>
求和
我们将 (Int) -> (整数)-> Int
lambda 用pure
函数然后我们应用 Option
`(*)`
作为对 Haskell 的
<*>
的致敬。
到目前为止,您可以看到 applicatives 可以让您做一些 很酷的技巧,但 monad 更强大、更灵活。什么时候使用其中一种?这显然取决于您的特定问题,但我们的一般建议是尽可能少地使用抽象。你可以从 functor 的 map
开始,然后是 applicative 的 ap
,最后是 monad 的 flatMap
。一切都可以用
flatMap
(如你所见 Option
, map< /code> 和
ap
是使用 flatMap
) 实现的,但大多数时候是 map
和 ap
可以更容易理解。
回到函数,我们可以让函数表现为应用程序。首先,我们应该添加一个纯函数:
首先,我们创建一个对象Function1
,作为函数类型(A)->; B
没有像我们使用 Option:
那样添加新的扩展函数的伴随对象
Function1.pure(t: T)
会将一个 T
值包装在一个函数中并返回它,而不管我们使用的参数。如果您有使用其他函数式语言的经验,您会将函数的 pure
识别为 identity
函数(更多关于 第十二章identity
函数>、Arrow 入门)。
让我们将 flatMap
,一个 ap
,添加到函数 (A) -> B
:
我们已经介绍了 map(transform: (B) -> C): (A) -> C
并且我们知道它表现为一个前向函数组合。如果您密切注意 flatMap
和 ap
,您会发现参数有点倒退(并且 ap
被实现为其他类型的所有其他 ap
函数)。
好吧,我们可以组合函数,这一点都不令人兴奋,因为我们已经用 map
做到了。但是函数的 ap
有一个小技巧。 我们可以访问原始参数:
访问函数组合中的原始参数在多种情况下很有用,例如调试和审计。