读书笔记《functional-kotlin》箭头类型
Option<T>
数据类型是存在的 representation 或 < span>absence 值 T
。在 Arrow 中,Option<T>
是一个有两个子类型的密封类,Some
T
和
None
的存在,以及表示值不存在的对象。
Option<T>
定义为密封类,不能有任何其他子类型;因此,编译器可以详尽地检查子句,如果这两种情况,
Some<T>
和
None
都被覆盖。
我知道(或者我假装知道)你此刻在想什么——为什么我需要 Option<T>
来表示 T
,如果在 Kotlin 中我们已经有 T
表示存在,而 T?
表示缺席?
你是对的。但是 Option
提供了比可空类型更多的价值,让我们直接跳到一个例子:
division
函数 接受三个参数——两个整数(a
、b
) 和一个分母 (
或 den
) 并返回一个 Pair<Int, Int>
,如果两个数字都可以被 < code class="literal">dennull
否则。
我们可以用 Option
来表达同样的算法:
函数, optionDivide
采用 nullable 结果来自除 and 使用 Option
返回>toOption() 扩展函数。
optionDivision
与division
相比没有大的变化,是同一个算法用不同的类型表示。如果我们停在这里,那么 Option<T>
不会在 nullables 之上提供额外的价值。幸运的是,事实并非如此。 Option
还有更多的使用方法:
Option
提供了几个函数来处理其内部值,在这种情况下,flatMap
(作为一个 monad)现在我们的代码看起来像短很多。
看看以下带有一些 Option<T>
函数的简短列表:
功能 |
说明 |
|
如果值 |
|
如果值 |
|
|
|
返回转换为 |
|
如果存在则返回值 |
|
一个转换函数(如 |
|
将值 |
理解是一种技术 可以按顺序计算任何类型(例如选项
, List
和其他),它包含一个 flatMap
函数并且可以提供一个 monad 的实例(稍后会详细介绍)。
在 Arrow 中,理解使用协程。是的,协程在在异步执行域之外很有用。
如果我们从前面的例子中勾勒出延续,它看起来像这样(这是一个有助于理解协程的心智模型)
Option.monad().binding
是协程构建器,bind()
函数是暂停函数。如果你没记错我们的协程章节,延续是暂停点之后的任何代码的表示(即,当调用暂停的函数时)。在我们的示例中,我们有两个暂停点和两个延续,当我们返回时(在最后一个块行),我们处于第二个延续,我们可以访问这两个值, aDiv< /code> 和
bDiv
。
将此算法作为延续来读取与我们的 flatMapDivision
函数非常相似。在幕后,Option.monad().binding
使用 Option.flatMap
和延续来创建理解;编译后,comprehensionDivision
和 flatMapDivision
大致是等价的。
ev()
方法 将在下一节解释。
Kotlin 的类型系统有一个限制——它不支持 Higher-Kinded Types (HKT )。无需过多讨论类型理论,HKT 是一种将其他 generic 值声明为类型参数的类型:
缺少 HKT 对于 Kotlin 关于functional 编程,因为许多高级功能结构和模式都使用它们。
Note
Arrow 团队正在研究 Kotlin 进化和增强过程 (KEEP )——添加新语言特性的社区过程,在 Kotlin 中称为类型类作为扩展 (https://github.com/Kotlin/KEEP/pull/87) 以支持 HKT 和其他功能。目前尚不清楚这个 KEEP(编码为 KEEP-87)是否会很快包含在 Kotlin 中,但现在是评论最多的提案,并引起了很多关注。目前尚不清楚细节,因为它仍在进行中,但有一丝希望。
Arrow 对这个问题的解决方案是通过一种称为基于证据的 HKT 的技术来模拟 HKT。
让我们看一个 Option<T>
声明:
Option<A>
用 @higherkind
注释,类似于 @lenses
从我们上一章开始;此注释用于生成代码以支持基于证据的 HKT。 Option<A>
扩展自 OptionKind<A>
:
OptionKind<A>
是 HK<OptionHK, A>
的类型别名,所有这些代码都是使用 @higherkind
注释处理器。 OptionHK
是一个不可实例化的类,用作 HK
and OptionKind
是一种 HKT 的中间表示。 Option.monad().binding
返回 OptionKind<T>
,这就是为什么我们需要调用 ev()
最后返回一个正确的 Option
HK
接口(higher-kinded 的简写)用于表示一个 arity 的 HKT一个到 HK5
用于 arity 5。在 HK<F, A>
, F< /code>代表类型,
A
是泛型参数,所以 Option<Int>
是 OptionKind<Int>
值为 HK<OptionHK, Int>
。
现在让我们看看 Functor<F>
:
Functor<F>
扩展了 TC
,一个标记接口,你可以猜到,它有一个 地图
函数。 map
函数接收 HK<F, A>
作为第一个参数和一个 lambda (A) -> B
将 A
的值转化为B
并转化为 HK<F, B>.
让我们创建我们的基本数据类型 Mappable
,它可以为 Functor
类型类提供实例:
我们的类, Mappable<T>
用 @higherkind
注释并扩展了 MappableKind< T>
且必须有伴生对象,是否为空无所谓。
现在,我们需要创建 Functor<F>
的实现:
我们的 MappableFunctorInstance
接口 扩展了Functor
@instance(Mappable ::类)
。在
map
函数中,我们使用第一个参数
MappableKind<A>
并使用它的
地图
功能。
@instance
注解会生成一个对象extending 接口, MappableFunctorInstance
。它将创建一个 Mappable.Companion.functor()
扩展函数 使用MappableFunctorInstance
的对象"literal">Mappable.functor() (这是我们可以使用 Option.monad()
的方式)。
另一种选择是让 Arrow 派生实例自动提供,前提是您的数据类型具有正确的功能:
@deriving
注释将生成DerivedMappableFunctorInstance
,通常您将手动编写。
现在,我们可以创建一个通用函数来使用我们的 Mappable
函子:
buildBicycle
函数将任何 HK<F, Int>
作为参数并应用函数 f
使用它的 Functor
实现,由函数 arrow.typeclasses.functor
返回并返回 HK
。
The function arrow.typeclass.functor
resolves at runtime, instances that adhere to the Functor<MappableHK>
requirement:
我们可以将 buildBicycle
与 Mappeable<Int>
或任何其他 HKT 类一起使用,例如 Option< ;T>
。
使用 Arrows 方法处理 HKT 的一个问题是它必须在运行时解析其实例。这是因为 Kotlin 不支持隐式,也不能在编译时解决类型类实例,因此在 KEEP-87 之前,Arrow 只能选择这种方法批准并包含在语言中:
因此,您可以拥有一个具有 map
函数但没有 Functor
实例的 HKT,但不能使用'不是编译错误:
使用 NotAFunctor
buildBicycle
会编译,但会抛出
ClassNotFoundException
运行时异常。
现在我们了解了 Arrow 的层次结构是如何工作的,我们可以介绍其他类。
Either<L, R>
是 representation 两种可能之一值 L
或 R
,但不能同时使用两者。 Either
是一个密封类(类似于 Option
),有两个子类型 Left<L>< /code> 和
Right<R>
。通常Either
用来表示可能失败的结果,用左边表示错误,用右边表示成功的结果。因为表示可能失败的操作是一种常见情况,Arrow 的 Either
是右偏的,换句话说,除非 it 被记录,否则所有操作都运行在右侧。
让我们将除法示例从 Option
转换为 Either
:
现在,我们不是返回 None
值,而是向用户返回有价值的信息:
在 eitherDivision
中,我们使用 Arrow 的 Tuple<A, B>
而不是 Kotlin 的 Pair< ;A,B>
。 Tuples 提供了比 Pair/Triple 更多的功能,从现在开始我们将使用它。要创建一个Tuple2
,可以使用扩展infix
函数, toT
。
功能 |
说明 |
|
使用 |
|
如果 |
|
如果 |
|
|
|
为 |
|
返回 |
|
如果是 |
|
如果是 |
|
|
|
|
|
返回 |
|
|
Either
有一个 monad 实现,所以我们可以调用绑定函数:
注意Either.monad<L>()
;对于 Either<L, R>
它必须定义 L
类型:
在我们的下一节中,我们将学习 monad 转换器。
Either
和 Option
使用简单,但 what 如果我们将两者结合起来会发生什么?
UserService.findAge
返回 要么
Left<String>
用于错误
访问
数据库或任何其他基础设施,
Right<None>
表示在数据库中未找到值,
Right<Some
要打印年龄,我们需要两个嵌套的折叠,不要太复杂。当我们需要进行访问多个值的操作时,问题就来了:
Monad 不会组合,因此这些操作的复杂性增长 非常迅速。但是,我们总是可以依靠理解,不是吗?现在,让我们看看以下代码:
这样比较好,返回类型没那么长,fold
比较好管理。让我们看一下以下代码片段中的嵌套推导:
现在,我们有相同类型的值和结果。但是我们还有另一个选择,monad 转换器。
monad 转换器 是两个可以作为一个执行的 monad 的组合。对于我们的示例,我们将使用 OptionT
,(Option Transformer的简写)作为Option
是嵌套在 Either
中的 monad 类型:
我们使用 OptionT.monad<EitherKindPartial<String>>().binding
。 EitherKindPartial<String>
monad 表示包装器类型是 Either<String, Option<T>>
.
在 binding
块中,我们对 Either<String, Option< 类型的值使用
(技术上关于 OptionT
T>>HK<HK<EitherHK, String>, Option<T>>
类型的值)调用 bind(): T
,在我们的例子中是 T
,是 Int
。
之前我们只使用了 ev()
方法,但是现在我们需要使用value()
方法 提取 OptionT
内部值。
在下一节中,我们将了解 Try
类型。
Try 是 computation 的表示这可能会也可能不会失败。 Try<A>
是一个密封类,有两个可能的子类——Failure
,代表失败和
Success
表示操作成功。
让我们用 Try
编写除法示例:
创建 Try
实例的最简单方法是使用 Try.invoke
运算符。如果里面的block抛出异常,会返回 Failure
;如果一切顺利, Success<Int>
,例如!!
算子 会抛出 < code class="literal">NPE 如果除法返回空值:
功能 |
说明 |
|
如果 |
|
如果操作成功并通过谓词 |
|
|
|
返回转换为 |
|
返回值 |
|
返回值 |
|
如果 |
|
如果 |
|
像函子一样转换函数。 |
|
对 |
|
对 |
|
|
|
为 |
|
为 |
|
转换成 |
|
转化为 |
flatMap
实现非常类似于 Either
和 Option
并显示值具有一组通用的名称和行为约定:
Try
也可以使用一元推导:
还有另一种使用 MonadError
实例的单子理解:
使用 monadError.bindingCatch
任何抛出异常的操作都会被提升到 Failure
,最后返回的结果被包装到 < code class="literal">试试<T>。 MonadError
也可用于 Option
和 Either
。
State 是一种结构,提供处理应用程序状态的功能方法。 State<S, A>
是对 S -> 的抽象。元组2
。 S代表状态类型, Tuple2
是结果,
S
表示新更新的状态,
A
表示函数返回。
我们可以从一个简单的例子开始,一个返回两件事的函数,一个价格和计算它的步骤。要计算价格,我们需要添加 20% 的VAT
,如果 price
value 则应用折扣超过某个阈值:
我们有一个类型别名 PriceLog
用于 MutableList<Tuple2<String, Double>>
。 PriceLog
将是我们的 State
表示;每个步骤用 Tuple2
我们的第一个函数 addVat(): State
代表第一步。我们使用 State
构建器编写函数,该构建器接收 PriceLog
,即应用任何步骤之前的状态,并且必须返回 Tuple2
,我们使用 Unit
因为此时我们不需要价格:
applyDiscount
函数是我们的第二步。我们在这里引入的唯一新元素是两个参数,一个用于 threshold
,另一个用于 discount
:
最后一步由函数finalPrice()
表示,现在我们返回 Double
而不是Unit
:
为了表示步骤的顺序,我们使用 monad 理解并顺序使用 State
函数。从一个函数到下一个函数,PriceLog
状态是隐式流动的(只是一些协程延续的魔法)。最后,我们得出最终价格。添加新步骤或切换现有步骤就像添加或移动线条一样简单:
要使用 calculatePrice
函数,您必须提供阈值和折扣值,然后以初始状态调用扩展函数 run
。如果您只对价格感兴趣,您可以使用 runA
或仅对历史记录, runS
。
State
在 corecursion 上是 beneficial ;我们可以用 State
重写我们的旧示例:
我们原来的unfold
函数使用了一个函数, f:(S) ->与
:State<S, T>
非常相似的对
而不是 lambda (S) -> Pair<T, S>?
,我们使用 State<S, Option<T>>
,我们使用 Option
,Sequence
为空,None
或递归调用 一些<T>
:
我们旧的阶乘函数使用 unfold
Pair<Long, Int>
和一个 lambda—— (对<Long, Int>) -> Pair<Long, Pair<Long, Int>>?
:
重构的阶乘使用 State<Tuple<Long, Int>, Option<Long>>
但内部逻辑几乎相同,虽然我们的新阶乘不使用null,这是一个显着的改进:
类似地,fib
使用带有 Triple
的展开(三重<Long,Long.Int>)->对<Long, Triple<Long, Long, Int>>?
:
而重构后的fib
使用 State
plus
,与
Tuple2<A, B>
和
C
将返回
Tuple3<A, B, C>
:
现在,我们可以使用我们的核心递归函数来生成序列。 State
还有很多其他用途,我们在这里无法介绍,例如 Message History来自企业集成模式(http://www.enterpriseintegrationpatterns.com/patterns/messaging/MessageHistory.html) 或在具有多个步骤的表单上导航,例如平面检查或长注册表。
Arrow 提供了许多数据类型和类型类,可以减少非常复杂的任务并提供一组标准的习语和表达式。在本章中,我们学习了如何使用 Option
对空值进行抽象,以及如何使用 Either
和 尝试
。我们创建了一个数据类型类,还学习了单子理解和转换。最后但同样重要的是,我们使用 State
来表示应用程序状态。
通过这一章,我们到达了这个旅程的终点,但请放心,这并不是你学习函数式编程旅程的终点。正如我们在第一章中所了解的,函数式编程就是使用函数作为构建块来创建复杂的程序。同样,通过您在这里学到的所有概念,您现在可以理解和掌握新的、令人兴奋的和更强大的想法。
现在,您开始了新的学习之旅。