对单子不科学

大家好

在Scala上进行了四年编程之后,我对monads的了解终于发展到可以不参考类别理论和经典monad来向他人解释的程度了-它只是内向资助者类别中的一个类人动物 ,这使程序员感到恐惧不比敌敌畏蟑螂更糟糕。

代码示例将用Kotlin编写, 它非常受欢迎,同时又很有功能(从字面意义上来说)。

让我们从函子的概念开始,这里是:

interface Functor<A> 

它是什么意思? 函数是对返回类型A的结果的任意计算的抽象。我们从如何创建新的函数,以及最重要的是如何计算其值A上抽象出来。特别是,一个函数可以隐藏在函数接口的后面具有任意数量的参数,不一定是纯函数。

仿函数实现的示例:

  • 不变的
  • 带有任意数量参数的函数,该函数返回类型A的结果
  • 状态伪随机生成器(Random)
  • 硬件随机数发生器
  • 从磁盘或网络读取对象
  • 异步计算-回调传递给functor实现,稍后再调用

除常数外,所有这些示例都具有一个重要的属性-它们是惰性的,即 创建函子时,计算本身不会发生,但在计算时会发生。

functor接口不允许从Functor<A>获取类型A的值,或者不允许从现有的Type A值创建新的Functor<A> A 但是即使有这样的限制,函子也不是没有用的-如果对于某些类型B我们可以将A转换为B (换句话说,有一个函数(a: A) -> B ),那么我们可以编写一个函数(f: Functor<A>) -> Functor<B>并将其命名为map

 interface Functor<A> { fun <B> map(f: (A) -> B): Functor<B> } 

与函子本身不同,map方法不能是任意函数:
- map((a) -> a)应该返回相同的函子
map((a) -> f(a)).map((b) -> g(b))必须与map(a -> g(f(a))

例如,我们实现一个函子,该函子返回包含一定数量的随机位的A值。 我们在Kotlin中的界面不能那么容易地使用(但是,如果需要,可以使用),因此我们将编写一个扩展方法:

 //  - ,     ,   map data class MyRandom<A>( val get: (bits: Int) -> A ) { companion object { val intRandom: MyRandom<Int> = MyRandom { Random.nextBits(it) } val hexRandom: MyRandom<String> = intRandom.map { it.toString(16) } } } //  map   fun <A, B> MyRandom<A>.map(f: (A) -> B): MyRandom<B> = MyRandom(get = {bits -> f(get(bits)) }) fun main(args: Array<String>) { println("random=" + MyRandom.intRandom.get(12)) //  random=1247 println("hexRandom=" + MyRandom.hexRandom.get(12)) //  hexRandom=c25 } 

带有有用map的函子的其他示例

  • 不可变List<A>
  • MyInputStream<A>
  • Optional<A>

现在您可以去monads。

monad是具有两个附加操作的函子。 首先,与函子不同,monad包含从常量创建的操作,此操作称为lift

 fun <A> lift(value: A): Monad<A> = TODO() 

第二个操作称为flatMap ,它比较复杂,因此首先我们将提供整个monad接口:

 interface Monad<A> { //   ,  map     - //    flatMap  lift fun <B> map(f: (A) -> B): Monad<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> Monad<B>): Monad<B> } fun <A> lift(value: A): Monad<A> = TODO() 

monad和函子之间最重要的区别在于monad可以彼此组合 ,生成新monad并从monad的实现方式中抽象出来-是否从磁盘读取,是否接受其他参数来计算其值,该值是否存在。 第二个要点-monad不是并行组合,而是顺序组合,从而可以根据第一个monad的结果添加逻辑。

一个例子:

 // ,     Int //       -      //           val readInt: Monad<Int> = TODO() // ,      -  fun readBytes(len: Int): Monad<ByteArray> = TODO() // ,     ,    val bytes: Monad<ByteArray> = readInt.flatMap {len -> if (len > 0) readBytes(len) //    -   else lift(ByteArray(0)) //  ,    } 

但是,在此示例中,没有提及网络。 同样,可以从文件或数据库中读取数据。 它们可以同步或异步读取,这里可能会有错误处理-一切都取决于monad的特定实现,代码本身将保持不变。

首先,该示例更为简单,即Option monad。 在kotlin中,并不是真正需要它,但是在Java / Scala中,它非常有用:

 data class Option<A>(val value: A?) { fun <B> map(f: (A) -> B): Option<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> Option<B>): Option<B> = when(value) { null -> Option(null) else -> f(value) } } fun <A> lift(value: A?): Option<A> = Option(value) fun mul(a: Option<Int>, b: Option<Int>): Option<Int> = a.flatMap { a -> b.map { b -> a * b } } fun main(args: Array<String>) { println(mul(Option(4), Option(5)).value) // 20 println(mul(Option(null), Option(5)).value) // null println(mul(Option(4), Option(null)).value) // null println(mul(Option(null), Option(null)).value) // null } 

作为pozakovyristy的monad,让我们用monad中的数据库结束工作:

 data class DB<A>(val f: (Connection) -> A) { fun <B> map(f: (A) -> B): DB<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> DB<B>): DB<B> = DB { conn -> f(this.f(conn)).f(conn) } } fun <A> lift(value: A): DB<A> = DB { value } fun select(id: Int): DB<String> = DB { conn -> val st = conn.createStatement() // .... TODO() } fun update(value: String): DB<Unit> = DB { conn -> val st = conn.createStatement() // .... TODO() } fun selectThenUpdate(id: Int): DB<Unit> = select(id).flatMap { value -> update(value) } fun executeTransaction(c: Connection): Unit { //  ,     //          val body: DB<Unit> = selectThenUpdate(42) //  ,   select  update body.f(c) c.commit() } 

兔子洞深吗?


monad种类繁多,但它们的主要目的是从所执行计算的一些细节中抽象出应用程序的业务逻辑:

  • 值可能不存在: data class Option<A>(value: A?)
  • 计算将失败: data class Either<Error, A>(value: Pair<Error?, A?>)
  • 计算可以是惰性的: data class Defer<A>(value: () -> A)
  • 或异步: java.util.concurrent.CompletableFuture<A>
  • 或具有功能状态: data class State<S, A>(value: (S) -> Pair<S, A>)

未回答的问题清单:

  • 应用函子-函子和单子之间的中间链接
  • 像monads这样的收藏
  • 单峰单子的组成-Arrow Gunsi,单子变压器
  • 顺序/遍历
  • 单子效应
  • 单子和递归,堆栈溢出,蹦床
  • 无标签最终编码
  • 艾奥·莫纳德
  • 通常是整个标准单子的动物园

接下来是什么?


arrow-kt.io
typelevel.org/cats/typeclasses.html
wiki.haskell.org/All_About_Monads

我的实验是Scala上成熟的FP风格的应用程序:
github.com/scf37/fpscala2

PS我想要一个小纸条,结果还是一如既往。

Source: https://habr.com/ru/post/zh-CN454534/


All Articles