vlambda博客
学习文章列表

读书笔记《functional-kotlin》箭头类型

Chapter 13. Arrow Types

Arrow 包含许多常规函数类型的实现,例如 OptionEitherTry,以及许多其他类型的类,例如 functor 和 monad。

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

  • Using Option to manage null
  • Either and Try to manage errors
  • Combinations and transformers
  • State to manage application state

Option


Option<T> 数据类型是存在的 representation 或 < span>absenceT。在 Arrow 中,Option<T> 是一个有两个子类型的密封类,Some ,一个数据类表示值 TNone 的存在,以及表示值不存在的对象。 Option<T> 定义为密封类,不能有任何其他子类型;因此,编译器可以详尽地检查子句,如果这两种情况, Some<T>None 都被覆盖。

我知道(或者我假装知道)你此刻在想什么——为什么我需要 Option<T> 来表示 T,如果在 Kotlin 中我们已经有 T 表示存在,而 T? 表示缺席?

你是对的。但是 Option 提供了比可空类型更多的价值,让我们直接跳到一个例子:

fun divide(num: Int, den: Int): Int? {
    return if (num % den != 0) {
        null
    } else {
        num / den
    }
}

fun division(a: Int, b: Int, den: Int): Pair<Int, Int>? {
    val aDiv = divide(a, den)
    return when (aDiv) {
        is Int -> {
            val bDiv = divide(b, den)
            when (bDiv) {
                is Int -> aDiv to bDiv
                else -> null
            }
        }
        else -> null
    }
}

division 函数 接受三个参数——两个整数(ab ) 和一个分母 (den) 并返回一个 Pair<Int, Int>,如果两个数字都可以被 < code class="literal">dennull 否则。

我们可以用 Option 来表达同样的算法:

import arrow.core.*
import arrow.syntax.option.toOption

fun optionDivide(num: Int, den: Int): Option<Int> = divide(num, den).toOption()

fun optionDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
   val aDiv = optionDivide(a, den)
   return when (aDiv) {
      is Some -> {
         val bDiv = optionDivide(b, den)
         when (bDiv) {
            is Some -> Some(aDiv.t to bDiv.t)
            else -> None
         }
      }
      else -> None
   }
}

函数, optionDivide 采用 nullable 结果来自除 and 使用 Option 返回>toOption() 扩展函数。

optionDivisiondivision相比没有大的变化,是同一个算法用不同的类型表示。如果我们停在这里,那么 Option<T> 不会在 nullables 之上提供额外的价值。幸运的是,事实并非如此。 Option还有更多的使用方法:

fun flatMapDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
   return optionDivide(a, den).flatMap { aDiv: Int ->
      optionDivide(b, den).flatMap { bDiv: Int ->
         Some(aDiv to bDiv)
      }
   }
}

Option 提供了几个函数来处理其内部值,在这种情况下,flatMap(作为一个 monad)现在我们的代码看起来像短很多。

看看以下带有一些 Option<T> 函数的简短列表:

功能

说明

exists(p :Predicate<T>): Boolean

如果值 T 存在,则返回谓词 p 结果,否则返回 null。

filter(p: Predicate<T>): Option<T>

如果值 T 存在并满足谓词 pSome<T> >,否则

 flatMap(f: (T) -> Option<T>): Option<T>

flatMap 转换函数(如 monad)。

<R> fold(ifEmpty: () -> R, some: (T) -> R): R

返回转换为 R 的值,为 None 调用 ifEmpty,为 None 调用一些code class="literal">一些<T>。

getOrElse(default:() -> T): T

如果存在则返回值 T,否则返回 default 结果。

<R> map(f: (T) -> R):选项

一个转换函数(如 functor)。

orNull(): T?

将值 T 作为可为空的 T? 返回。

除法的最后一个实现 将使用推导:

import arrow.typeclasses.binding

fun comprehensionDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
   return Option.monad().binding {
      val aDiv: Int = optionDivide(a, den).bind()
      val bDiv: Int = optionDivide(b, den).bind()
      aDiv to bDiv
   }.ev()
}

理解是一种技术 可以按顺序计算任何类型(例如选项 , List 和其他),它包含一个 flatMap 函数并且可以提供一个 monad 的实例(稍后会详细介绍)。

在 Arrow 中,理解使用协程。是的,协程在异步执行域之外很有用。

如果我们从前面的例子中勾勒出延续,它看起来像这样(这是一个有助于理解协程的心智模型)

fun comprehensionDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
   return Option.monad().binding {
      val aDiv: Int = optionDivide(a, den).bind()
// start continuation 1
         val bDiv: Int = optionDivide(b, den).bind()
//start continuation 2
            aDiv to bDiv
//end continuation 2
      // end continuation 1
   }.ev()
}

Option.monad().binding 是协程构建器,bind() 函数是暂停函数。如果你没记错我们的协程章节,延续是暂停点之后的任何代码的表示(即,当调用暂停的函数时)。在我们的示例中,我们有两个暂停点和两个延续,当我们返回时(在最后一个块行),我们处于第二个延续,我们可以访问这两个值, aDiv< /code> 和 bDiv

将此算法作为延续来读取与我们的 flatMapDivision 函数非常相似。在幕后,Option.monad().binding 使用 Option.flatMap 和延续来创建理解;编译后,comprehensionDivisionflatMapDivision 大致是等价的。

 ev() 方法 将在下一节解释。

Arrow's type hierarchy


Kotlin 的类型系统有一个限制——它不支持 Higher-Kinded Types (HKT )。无需过多讨论类型理论,HKT 是一种将其他 generic 值声明为类型参数的类型:

class MyClass<T>() //Valid Kotlin code

class MyHigherKindedClass<K<T>>() //Not valid kotlin code

缺少 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>声明:

package arrow.core

import arrow.higherkind
import java.util.*

/**
 * Represents optional values. Instances of `Option`
 * are either an instance of $some or the object $none.
 */
@higherkind
sealed class Option<out A> : OptionKind<A> {
  //more code goes here

Option<A>@higherkind 注释,类似于 @lenses 从我们上一章开始;此注释用于生成代码以支持基于证据的 HKT。 Option<A> 扩展自 OptionKind<A>

package arrow.core

class OptionHK private constructor()
typealias OptionKind<A> = arrow.HK<OptionHK, A>

@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
inline fun <A> OptionKind<A>.ev(): Option<A> =
  this as Option<A>

OptionKind<A>HK<OptionHK, A> 的类型别名,所有这些代码都是使用 @higherkind 注释处理器。 OptionHK 是一个不可实例化的类,用作 HK and OptionKind 是一种 HKT 的中间表示。 Option.monad().binding 返回 OptionKind<T>,这就是为什么我们需要调用 ev() 最后返回一个正确的 Option

package arrow

interface HK<out F, out A>

typealias HK2<F, A, B> = HK<HK<F, A>, B>

typealias HK3<F, A, B, C> = HK<HK2<F, A, B>, C>

typealias HK4<F, A, B, C, D> = HK<HK3<F, A, B, C>, D>

typealias HK5<F, A, B, C, D, E> = HK<HK4<F, A, B, C, D>, E>

HK 接口(higher-kinded 的简写)用于表示一个 arity 的 HKT一个到 HK5 用于 arity 5。在 HK<F, A>, F< /code>代表类型,A是泛型参数,所以 Option<Int>是 OptionKind<Int> 值为 HK<OptionHK, Int>

现在让我们看看 Functor<F>

package arrow.typeclasses

import arrow.*

@typeclass
interface Functor<F> : TC {

    fun <A, B> map(fa: HK<F, A>, f: (A) -> B): HK<F, B>

}

Functor<F> 扩展了 TC,一个标记接口,你可以猜到,它有一个 地图函数。 map 函数接收 HK<F, A> 作为第一个参数和一个 lambda (A) -> B 将 A的值转化为B并转化为 HK<F, B>.

让我们创建我们的基本数据类型 Mappable,它可以为 Functor 类型类提供实例:

import arrow.higherkind

@higherkind
class Mappable<T>(val t: T) : MappableKind<T> {
   fun <R> map(f: (T) -> R): Mappable<R> = Mappable(f(t))

   override fun toString(): String = "Mappable(t=$t)"

   companion object
}

我们的类, Mappable<T>@higherkind 注释并扩展了 MappableKind< T> 且必须有伴生对象,是否为空无所谓。

现在,我们需要创建 Functor<F> 的实现:

import arrow.instance
import arrow.typeclasses.Functor

@instance(Mappable::class)
interface MappableFunctorInstance : Functor<MappableHK> {
   override fun <A, B> map(fa: MappableKind<A>, f: (A) -> B): Mappable<B> {
      return fa.ev().map(f)
   }
}

我们的 MappableFunctorInstance 接口 扩展了Functor 并用 @instance(Mappable ::类)。在 map 函数中,我们使用第一个参数 MappableKind<A> 并使用它的 地图 功能。

@instance 注解会生成一个对象extending 接口,  MappableFunctorInstance。它将创建一个 Mappable.Companion.functor()扩展函数 使用MappableFunctorInstance的对象"literal">Mappable.functor() (这是我们可以使用 Option.monad() 的方式)。

另一种选择是让 Arrow 派生实例自动提供,前提是您的数据类型具有正确的功能:

import arrow.deriving 

@higherkind
@deriving(Functor::class)
class DerivedMappable<T>(val t: T) : DerivedMappableKind<T> {
   fun <R> map(f: (T) -> R): DerivedMappable<R> = DerivedMappable(f(t))

   override fun toString(): String = "DerivedMappable(t=$t)"

   companion object
}

@deriving 注释将生成DerivedMappableFunctorInstance,通常您将手动编写。

现在,我们可以创建一个通用函数来使用我们的 Mappable 函子:

import arrow.typeclasses.functor

inline fun <reified F> buildBicycle(mapper: HK<F, Int>,
                           noinline f: (Int) -> Bicycle,
                           FR: Functor<F> = functor()): HK<F, Bicycle> = FR.map(mapper, f)

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:

fun main(args: Array<String>) {

   val mappable: Mappable<Bicycle> = buildBicycle(Mappable(3), ::Bicycle).ev()
   println("mappable = $mappable") //Mappable(t=Bicycle(gears=3))

   val option: Option<Bicycle> = buildBicycle(Some(2), ::Bicycle).ev()
   println("option = $option") //Some(Bicycle(gears=2))

   val none: Option<Bicycle> = buildBicycle(None, ::Bicycle).ev()
   println("none = $none") //None

}

我们可以将 buildBicycleMappeable<Int> 或任何其他 HKT 类一起使用,例如 Option< ;T>

使用 Arrows 方法处理 HKT 的一个问题是它必须在运行时解析其实例。这是因为 Kotlin 不支持隐式,也不能在编译时解决类型类实例,因此在 KEEP-87 之前,Arrow 只能选择这种方法批准并包含在语言中:

@higherkind
class NotAFunctor<T>(val t: T) : NotAFunctorKind<T> {
   fun <R> map(f: (T) -> R): NotAFunctor<R> = NotAFunctor(f(t))

   override fun toString(): String = "NotAFunctor(t=$t)"
}

因此,您可以拥有一个具有 map 函数但没有 Functor 实例的 HKT,但不能使用'不是编译错误:

fun main(args: Array<String>) {

   val not: NotAFunctor<Bicycle> = buildBicycle(NotAFunctor(4), ::Bicycle).ev()
   println("not = $not")

}

使用 NotAFunctor  函数调用 buildBicycle 会编译,但会抛出 ClassNotFoundException 运行时异常。

现在我们了解了 Arrow 的层次结构是如何工作的,我们可以介绍其他类。

Either


Either<L, R>representation 两种可能之一值 LR,但不能同时使用两者。 Either 是一个密封类(类似于 Option),有两个子类型 Left<L>< /code> 和 Right<R>。通常Either用来表示可能失败的结果,用左边表示错误,用右边表示成功的结果。因为表示可能失败的操作是一种常见情况,Arrow 的 Either 是右偏的,换句话说,除非 it 被记录,否则所有操作都运行在右侧。

让我们将除法示例从 Option 转换为 Either

import arrow.core.Either
import arrow.core.Either.Right
import arrow.core.Either.Left

fun eitherDivide(num: Int, den: Int): Either<String, Int> {
   val option = optionDivide(num, den)
   return when (option) {
      is Some -> Right(option.t)
      None -> Left("$num isn't divisible by $den")
   }
}

现在,我们不是返回 None 值,而是向用户返回有价值的信息:

import arrow.core.Tuple2

fun eitherDivision(a: Int, b: Int, den: Int): Either<String, Tuple2<Int, Int>> {
   val aDiv = eitherDivide(a, den)
   return when (aDiv) {
      is Right -> {
         val bDiv = eitherDivide(b, den)
         when (bDiv) {
            is Right -> Right(aDiv.getOrElse { 0 } toT bDiv.getOrElse { 0 })
            is Left -> bDiv as Either<String, Nothing>
         }
      }
      is Left -> aDiv as Either<String, Nothing>
   }
}

eitherDivision 中,我们使用 Arrow 的 Tuple<A, B> 而不是 Kotlin 的 Pair< ;A,B>。 Tuples 提供了比 Pair/Triple 更多的功能,从现在开始我们将使用它。要创建一个Tuple2,可以使用扩展infix函数, toT

接下来,short 列表 Either<L, R>功能:

功能

说明

bimap(fa:(L) -> T, fb:(R) -> X): 任意一个

使用 faLeftfb正确 返回 Either<T, X>

contains(elem:R): Boolean

如果 Right 值与 elem 参数相同,则返回 truefalseLeft

exists(p:Predicate<R>):Boolean

如果 Right,返回 Predicate p 结果,对于 false ="literal">左。

flatMap(f: (R) -> Either ): Either

flatMap 函数,如 Monad 中的函数,使用   Right

fold(fa: (L) -> T, fb: (R) -> T): T

LeftfaTRight 的literal">fb。

getOrElse(default:(L) -> R): R

返回 Right 值,或 default 函数的结果。

isLeft(): 布尔值

如果是 Left and false 的实例,则返回 true

isRight(): 布尔值

如果是 Right and false 的实例,则返回 true

map(f: (R) -> T): 任一个

map 函数,如 Functor,如果 Right,使用函数 f 将其转换为 Right ,如果 Left 返回相同的值没有转变。

mapLeft(f: (L) -> T): 任一个<T, R>

map 函数,如 Functor,如果 Left,使用函数 f 将其转换为 Left<T>,如果 Right,则返回相同的值没有转变。

swap(): 要么<R, L>

返回 Either,其类型和值已交换。

toOption(): Option

Some<T> for Right and None for

flatMap 版本 looks 符合预期:

fun flatMapEitherDivision(a: Int, b: Int, den: Int): Either<String, Tuple2<Int, Int>> {
   return eitherDivide(a, den).flatMap { aDiv ->
      eitherDivide(b, den).flatMap { bDiv ->
         Right(aDiv toT bDiv)
      }
   }
}

Either 有一个 monad 实现,所以我们可以调用绑定函数:

fun comprehensionEitherDivision(a: Int, b: Int, den: Int): Either<String, Tuple2<Int, Int>> {
   return Either.monad<String>().binding {
      val aDiv = eitherDivide(a, den).bind()
      val bDiv = eitherDivide(b, den).bind()

      aDiv toT bDiv
   }.ev()

注意Either.monad<L>();对于 Either<L, R> 它必须定义 L 类型:

fun main(args: Array<String>) {
   eitherDivision(3, 2, 4).fold(::println, ::println) //3 isn't divisible by 4
}

在我们的下一节中,我们将学习 monad 转换器。

Monad transformers


EitherOption 使用简单,但 what 如果我们将两者结合起来会发生什么?

object UserService {

   fun findAge(user: String): Either<String, Option<Int>> {
       //Magic  
   }
}

UserService.findAge 返回 要么 ; Left<String> 用于错误 访问 数据库或任何其他基础设施, Right<None> 表示在数据库中未找到值, Right<Some 表示找到值:

import arrow.core.*
import arrow.syntax.function.pipe

fun main(args: Array<String>) {
 val anakinAge: Either<String, Option<Int>> = UserService.findAge("Anakin")

 anakinAge.fold(::identity, { op ->
         op.fold({ "Not found" }, Int::toString)
     }) pipe ::println 
}

要打印年龄,我们需要两个嵌套的折叠,不要太复杂。当我们需要进行访问多个值的操作时,问题就来了:

import arrow.core.*
import arrow.syntax.function.pipe
import kotlin.math.absoluteValue

fun main(args: Array<String>) {
   val anakinAge: Either<String, Option<Int>> = UserService.findAge("Anakin")
   val padmeAge: Either<String, Option<Int>> = UserService.findAge("Padme")

   val difference: Either<String, Option<Either<String, Option<Int>>>> = anakinAge.map { aOp ->
      aOp.map { a ->
         padmeAge.map { pOp ->
            pOp.map { p ->
               (a - p).absoluteValue
            }
         }
      }
   }

   difference.fold(::identity, { op1 ->
      op1.fold({ "Not Found" }, { either ->
         either.fold(::identity, { op2 -> 
            op2.fold({ "Not Found" }, Int::toString) })
      })
   }) pipe ::println
}

Monad 不会组合,因此这些操作的复杂性增长 非常迅速。但是,我们总是可以依靠理解,不是吗?现在,让我们看看以下代码:

import arrow.core.*
import arrow.syntax.function.pipe
import arrow.typeclasses.binding
import kotlin.math.absoluteValue


fun main(args: Array<String>) {
   val anakinAge: Either<String, Option<Int>> = UserService.findAge("Anakin")
   val padmeAge: Either<String, Option<Int>> = UserService.findAge("Padme")

   val difference: Either<String, Option<Option<Int>>> = Either.monad<String>().binding {
      val aOp: Option<Int> = anakinAge.bind()
      val pOp: Option<Int> = padmeAge.bind()
      aOp.map { a ->
         pOp.map { p ->
            (a - p).absoluteValue
         }
      }
   }.ev()

   difference.fold(::identity, { op1 ->
      op1.fold({ "Not found" }, { op2 ->
         op2.fold({ "Not found" }, Int::toString) }) }) pipe ::println
}

 这样比较好,返回类型没那么长,fold比较好管理。让我们看一下以下代码片段中的嵌套推导:

fun main(args: Array<String>) {
   val anakinAge: Either<String, Option<Int>> = UserService.findAge("Anakin")
   val padmeAge:  Either<String, Option<Int>> = UserService.findAge("Padme")

   val difference: Either<String, Option<Int>> = Either.monad<String>().binding {
      val aOp: Option<Int> = anakinAge.bind()
      val pOp: Option<Int> = padmeAge.bind()
      Option.monad().binding {
         val a: Int = aOp.bind()
         val p: Int = pOp.bind()
         (a - p).absoluteValue
      }.ev()
   }.ev()

   difference.fold(::identity, { op ->
      op.fold({ "Not found" }, Int::toString)
   }) pipe ::println
}

现在,我们有相同类型的值和结果。但是我们还有另一个选择,monad 转换器。

monad 转换器 是两个可以作为一个执行的 monad 的组合。对于我们的示例,我们将使用 OptionT,(Option Transformer的简写)作为Option 是嵌套在 Either 中的 monad 类型:

import arrow.core.*
import arrow.data.OptionT
import arrow.data.monad
import arrow.data.value
import arrow.syntax.function.pipe
import arrow.typeclasses.binding
import kotlin.math.absoluteValue


fun main(args: Array<String>) {
   val anakinAge: Either<String, Option<Int>> = UserService.findAge("Anakin")
   val padmeAge: Either<String, Option<Int>> = UserService.findAge("Padme")

   val difference: Either<String, Option<Int>> = OptionT.monad<EitherKindPartial<String>>().binding {
      val a: Int = OptionT(anakinAge).bind()
      val p: Int = OptionT(padmeAge).bind()
      (a - p).absoluteValue
   }.value().ev()

   difference.fold(::identity, { op ->
      op.fold({ "Not found" }, Int::toString)
   }) pipe ::println
}

我们使用 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


Trycomputation 的表示这可能会也可能不会失败。 Try<A> 是一个密封类,有两个可能的子类——Failure ,代表失败和 Success 表示操作成功。

让我们用 Try 编写除法示例:

import arrow.data.Try

fun tryDivide(num: Int, den: Int): Try<Int> = Try { divide(num, den)!! }

 创建 Try 实例的最简单方法是使用 Try.invoke 运算符。如果里面的block抛出异常,会返回 Failure;如果一切顺利, Success<Int>,例如!! 算子 会抛出 < code class="literal">NPE 如果除法返回空值:

fun tryDivision(a: Int, b: Int, den: Int): Try<Tuple2<Int, Int>> {
   val aDiv = tryDivide(a, den)
   return when (aDiv) {
      is Success -> {
         val bDiv = tryDivide(b, den)
         when (bDiv) {
            is Success -> {
               Try { aDiv.value toT bDiv.value }
            }
            is Failure -> Failure(bDiv.exception)
         }
      }
      is Failure -> Failure(aDiv.exception)
   }
}

我们来看一个short列表 Try<T> 功能:

功能

说明

exists(p: Predicate<T>): Boolean

如果 Success<T> 返回 p 结果,在 Failure 上总是返回 <代码类="literal">假。

filter(p:  Predicate<T>): Try<T>

如果操作成功并通过谓词 p,则返回 Success<T>,否则返回 失败代码>。

<R> flatMap(f: (T) -> Try<R>): Try<R>

flatMap 函数与 monad 中的一样。

<R> fold(fa: (Throwable) -> R, fb:(T) -> R): R

返回转换为 R 的值,如果 Failure 则调用 fa

getOrDefault(default: () -> T): T

返回值 T,如果 Failure 则调用默认值。

getOrElse(default: (Throwable) -> T): T

返回值 T,如果 Failure 则调用默认值。

isFailure(): 布尔值

如果 Failure 则返回 true,否则返回 false

isSuccess(): 布尔值

如果 Success 则返回 true,否则返回 false

<R> map(f: (T) -> R): 试试

像函子一样转换函数。

onFailure(f: (Throwable) -> Unit): Try<T>

Failure 采取行动。

onSuccess(f: (T) -> Unit): Try

成功采取行动。

orElse(f: () -> Try<T>): Try<T>

Successf 结果返回 Failure

recover(f: (Throwable) -> T): Try<T>

Failure 转换 map 函数。

recoverWith(f: (Throwable) -> Try<T>): Try<T>

Failure 转换 flatMap 函数。

toEither() : Either

转换成 Either——FailureLeft<Throwable>成功 正确

toOption(): Option

转化为 Option——FailureNone成功<T>一些<T>

 

flatMap 实现非常类似于 EitherOption 并显示值具有一组通用的名称和行为约定:

fun flatMapTryDivision(a: Int, b: Int, den: Int): Try<Tuple2<Int, Int>> {
   return tryDivide(a, den).flatMap { aDiv ->
      tryDivide(b, den).flatMap { bDiv ->
         Try { aDiv toT bDiv }
      }
   }
}

Try 也可以使用一元推导:

fun comprehensionTryDivision(a: Int, b: Int, den: Int): Try<Tuple2<Int, Int>> {
   return Try.monad().binding {
      val aDiv = tryDivide(a, den).bind()
      val bDiv = tryDivide(b, den).bind()
      aDiv toT bDiv
   }.ev()
}

还有另一种使用 MonadError 实例的单子理解:

fun monadErrorTryDivision(a: Int, b: Int, den: Int): Try<Tuple2<Int, Int>> {
   return Try.monadError().bindingCatch {
      val aDiv = divide(a, den)!!
      val bDiv = divide(b, den)!!
      aDiv toT bDiv
   }.ev()
}

使用 monadError.bindingCatch 任何抛出异常的操作都会被提升到 Failure,最后返回的结果被包装到 < code class="literal">试试<T>。 MonadError 也可用于 OptionEither

State


State 是一种结构,提供处理应用程序状态的功能方法。 State<S, A> 是对 S -> 的抽象。元组2 S代表状态类型, Tuple2  是结果, S 表示新更新的状态, A 表示函数返回。

我们可以从一个简单的例子开始,一个返回两件事的函数,一个价格和计算它的步骤。要计算价格,我们需要添加 20% 的VAT,如果 price value 则应用折扣超过某个阈值:

import arrow.core.Tuple2
import arrow.core.toT
import arrow.data.State

typealias PriceLog = MutableList<Tuple2<String, Double>>

fun addVat(): State<PriceLog, Unit> = State { log: PriceLog ->
    val (_, price) = log.last()
    val vat = price * 0.2
    log.add("Add VAT: $vat" toT price + vat)
    log toT Unit
}

我们有一个类型别名 PriceLog 用于 MutableList<Tuple2<String, Double>>PriceLog 将是我们的 State 表示;每个步骤用 Tuple2 表示。

我们的第一个函数 addVat(): State 代表第一步。我们使用 State 构建器编写函数,该构建器接收 PriceLog,即应用任何步骤之前的状态,并且必须返回 Tuple2 ,我们使用 Unit 因为此时我们不需要价格:

fun applyDiscount(threshold: Double, discount: Double): State<PriceLog, Unit> = State { log ->
    val (_, price) = log.last()
    if (price > threshold) {
        log.add("Applying -$discount" toT price - discount)
    } else {
        log.add("No discount applied" toT price)
    }
    log toT Unit
}

applyDiscount 函数是我们的第二步。我们在这里引入的唯一新元素是两个参数,一个用于 threshold,另一个用于 discount

fun finalPrice(): State<PriceLog, Double> = State { log ->
    val (_, price) = log.last()
    log.add("Final Price" toT price)
    log toT price
}

最后一步由函数finalPrice()表示,现在我们返回 Double而不是Unit

import arrow.data.ev
import arrow.instances.monad
import arrow.typeclasses.binding

fun calculatePrice(threshold: Double, discount: Double) = State().monad<PriceLog>().binding {
    addVat().bind() //Unit
    applyDiscount(threshold, discount).bind() //Unit
    val price: Double = finalPrice().bind()
    price
}.ev()

为了表示步骤的顺序,我们使用 monad 理解并顺序使用 State 函数。从一个函数到下一个函数,PriceLog 状态是隐式流动的(只是一些协程延续的魔法)。最后,我们得出最终价格。添加新步骤或切换现有步骤就像添加或移动线条一样简单:

import arrow.data.run
import arrow.data.runA

fun main(args: Array<String>) {
    val (history: PriceLog, price: Double) = calculatePrice(100.0, 2.0).run(mutableListOf("Init" toT 15.0))
    println("Price: $price")
    println("::History::")
    history
            .map { (text, value) -> "$text\t|\t$value" }
            .forEach(::println)

    val bigPrice: Double = calculatePrice(100.0, 2.0).runA(mutableListOf("Init" toT 1000.0))
    println("bigPrice = $bigPrice")
}

要使用 calculatePrice 函数,您必须提供阈值和折扣值,然后以初始状态调用扩展函数 run。如果您只对价格感兴趣,您可以使用 runA 或仅对历史记录, runS

Note

避免使用 State 的问题。不要将扩展函数 arrow.data.run 与扩展函数混淆, kotlin.run(默认导入)。

Corecursion with State

State 在 corecursion 上是 beneficial ;我们可以用 State重写我们的旧示例:

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函数使用了一个函数, f:(S) ->与 State<S, T>非常相似的对 ?

fun <T, S> unfold(s: S, state: State<S, Option<T>>): Sequence<T> {
    val (actualState: S, value: Option<T>) = state.run(s)
    return value.fold(
            { sequenceOf() },
            { t ->
                sequenceOf(t) + unfold(actualState, state)
            })
}

而不是 lambda (S) -> Pair<T, S>?,我们使用 State<S, Option<T>>,我们使用 OptionSequence 为空,None 或递归调用 一些<T>

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

我们旧的阶乘函数使用 unfold  Pair<Long, Int> 和一个 lambda—— (对<Long, Int>) -> Pair<Long, Pair<Long, Int>>?:

import arrow.syntax.option.some

fun factorial(size: Int): Sequence<Long> {
    return sequenceOf(1L) + unfold(1L toT 1, State { (acc, n) ->
        if (size > n) {
            val x = n * acc
            (x toT n + 1) toT x.some()
        } else {
            (0L toT 0) toT None
        }
    })
}

重构的阶乘使用 State<Tuple<Long, Int>, Option<Long>> 但内部逻辑几乎相同,虽然我们的新阶乘不使用null,这是一个显着的改进:

fun fib(size: Int): Sequence<Long> {
   return sequenceOf(1L) + unfold(Triple(0L, 1L, 1)) { (cur, next, n) ->
      if (size > n) {
         val x = cur + next
         (x) to Triple(next, x, n + 1)
      }
      else
         null
   }
}

类似地,fib 使用带有 Triple 和 lambda 的展开(三重<Long,Long.Int>)->对<Long, Triple<Long, Long, Int>>?:

import arrow.syntax.tuples.plus

fun fib(size: Int): Sequence<Long> {
    return sequenceOf(1L) + unfold((0L toT 1L) + 1, State { (cur, next, n) ->
        if (size > n) {
            val x = cur + next
            ((next toT x) + (n + 1)) toT x.some()
        } else {
            ((0L toT 0L) + 0) toT None
        }
    })
}

而重构后的fib使用 State , Option 。密切关注扩展运算符函数 plus,与  Tuple2<A, B>C 将返回  Tuple3<A, B, C>

fun main(args: Array<String>) {
    factorial(10).forEach(::println)
    fib(10).forEach(::println)
}

现在,我们可以使用我们的核心递归函数来生成序列。 State 还有很多其他用途,我们在这里无法介绍,例如 Message History来自企业集成模式http://www.enterpriseintegrationpatterns.com/patterns/messaging/MessageHistory.html) 或在具有多个步骤的表单上导航,例如平面检查或长注册表。

Summary


Arrow 提供了许多数据类型和类型类,可以减少非常复杂的任务并提供一组标准的习语和表达式。在本章中,我们学习了如何使用 Option 对空值进行抽象,以及如何使用 Either尝试。我们创建了一个数据类型类,还学习了单子理解和转换。最后但同样重要的是,我们使用 State 来表示应用程序状态。

通过这一章,我们到达了这个旅程的终点​​,但请放心,这并不是你学习函数式编程旅程的终点​​。正如我们在第一章中所了解的,函数式编程就是使用函数作为构建块来创建复杂的程序。同样,通过您在这里学到的所有概念,您现在可以理解和掌握新的、令人兴奋的和更强大的想法。 

现在,您开始了新的学习之旅。