Kotlin(十七)函数式编程<3>

函数式通用结构设计

介绍一个非常让人恶心的专业术语,Monad。(单子)
Monad 无非就是个自函子范畴上的幺半群(Monoid)

百科上说: 在范畴论中,函子(functor)是范畴间的一类映射,通俗地说,是范畴间的同态。

我前面文章说,理解函子可以理解,高阶类型的参数之间的映射。

百科上说: 幺半群,是指在抽象代数此一数学分支中,幺半群是指一个带有可结合二元运算和单位元的代数结构。

简单Kotlin里理解:一方面是一个简单的Typeclass;另一方面,Monoid 用来描述一种代数,遵循了Monoid法则,即结合律和同一律。

说了这么多,解释一下数学专业属于,其实还是,有点含糊,不理它,但是不影响我们理解。

1. 什么是Monoid

  • 一个抽象类型A
  • 一个满足结合律的二元操作,(接受任何两个A类型的参数,返回一个A类型的结果)
  • 一个单元zero,同样也是一A类型的一个值
  • 结合律。append(a,append(b,c))==append(append(a,b),c),等式对于任何A类型的值(a,b,c)均成立
  • 同一律。append(a,zero)== a ,append(zero,a) == a,单元zero与任何A类型的值(a)的append操作,结果都等于a。
interface Monoid<A> {
    fun zero(): A
    fun A.append(b: A): A
}

我们Monoid做什么,举个小例子,字符串拼接操作

object stringConcatMonoid : Monoid<String> {
    override fun zero(): String = ""
    override fun String.append(b: String): String = this + b
}
  • 抽象类型A具体话String
  • 任何三个字符串拼接满足结合律。如:(“起灵” + “zcwfeng”)+“Kotlin” == “起灵” + (“zcwfeng” + “Kotlin”)
  • 单元zero为空字符串,zero=“”

2. Monoid 和 折叠列表

回顾,上篇文章的List定义

sealed class List<out A> : Kind<List.K, A> {
  object K
}
object Nil : List<Nothing>()
data class Cons<A>(val head: A, val tail: List<A>) : List<A>()

扩展一个sum方法,支持指定的一种二元操作,对列表元素操作。和上个文章说的ListFodable,这也是一个典型的fold操作

interface Foldable<F> {
  fun <A, B> Kind<F, A>.fold(init: B): ((B, A) -> B) -> B
}

object ListFoldable : Foldable<List.K> {
  override fun <A, B> Kind<List.K, A>.fold(init: B): ((B, A) -> B) -> B = { f ->
    fun fold0(l: List<A>, v: B): B {
      return when (l) {
        is Cons -> {
          fold0(l.tail, f(v, l.head))
        }
        else -> v
      }
    }
    fold0(this.unwrap(), init)
  }
}

查看前“Kotlin(十七)函数式编程<2>” 相关内容

fun <A> List<A>.sum(ma: Monoid<A>): A {
  val fa = this
  return ListFoldable.run {
    fa.fold(ma.zero())({ s, i ->
      ma.run {
        s.append(i)
      }
    })
  }
}

sum方法接受Monoid<A> 类型参数ma。Monoid抽象结构非常适合fold操作。回顾下Kotlin里面fold在标准库定义

public inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R): R

stringConcatMonoid 来写个测试

println(
                Cons(
                    "Dive ",
                    Cons(
                        "into ",
                        Cons("Kotlin", Nil)
                    )
                ).sum(stringConcatMonoid)
            )
结果:Dive into Kotlin

这里只是理论基础概念,复杂业务还需要好好考虑。

3. Monad

用Monoid<A>,Monoid<B>组合出一个新的Monoid<C>,这个新的Monoid依旧遵循Monoid法则,及满足同一律和结合律。
好处,我们遵循数学定理一样组合,无需关心过程的具体类型(A,B)最终导出的结果依旧遵循正确法则,省去了测试的工作。

(1) 函子定律

定义类型Kind<F,A>定义map操作,返回另一个类型Kind<F,B>.回顾Functor实现

// 模拟高阶类型
interface Kind<out F, out A>

interface Functor<F> {
    fun <A, B> Kind<F, A>.map(f: (A) -> B): Kind<F, B>
}

这里的类型List.K 替代F,代表一个列表容器,实际上F可以是其他的类型构造器,如

  • Kint<Option.K,A> 可空或者存在的高阶类型
  • Kind<Effect.k,A>拥有副作用的高阶类型
  • Kind<Parser.K,A>代表解析器的高阶类型
object ParserFunctor:Functor<Parser.K>{
  override def fun<A,B> Kind<Parser.K,A>.map(f:(A) -> B):Kind<Parser.K,B>
...
}

同一律法则
假设有一个identify函数,接受A类型参数,返回结果还是a

fun identify<A>(a:A) = a
ListFunctor.fun{
  println(Cons(1,Nil).map{identity(it)})
}

(2) 用map进行组合满足结合律。
函数f进行map的结果,应用函数g进行map,这个操作最终得到的结果与直接函子实例用两个函数组合的新函数进行map的结果相同

fun f(a: Int) = a + 1
            fun g(a: Int) = a * 2
            ListFunctor.run {
                val r1 = Cons(1, Nil)
                    .map { f(it) }.map { g(it) }

                var r2 = Cons(1, Nil).map { g(f(it)) }
                println(r1 == r2)
            }
结果:true

我们把告诫类型看做一个管道Kind<F,A>,Functor 提供了可能,支持管道内状态进行转化操作,简化表示为map操作,

fun <A, B> Kind<F, A>.map(f: (A) -> B): Kind<F, B>

新的管道规格保持不变,旧的容器依旧保持不变,利用递归思想(类似Pair构建出List),类似贪吃蛇可以创造出无尽的列表,用函数支持

fun <A, B, C> map2(fa: Kind<F, A>, fb: Kind<F, B>, f: (A, B) -> C): Kind<F, C>

实际业务副作用不可避免,如果我们把副作用限制在管道容器内,管道看做一个拥有原子性整体,那么依旧符合引用透明性。我们可以将相同容器内的副作用利用函数f组合,尽量推迟到最后执行,就是典型函数式编程。

(3)flatMap 实现复杂的组合

map2操作,会得到一个嵌套容器的结构。Kind<F,A>进行map,应用一个返回Kind<F,B>的函数,那么结果Kind<F,Kind<F,B>>.我们需要一个flattern的操作把嵌套容器的F提取出来,转化为Kind<F,B>

Kotlin 支持flatten操作的flatMap可以看成map与flatten的结合操作。可行思路就是给我们之前的高阶类型扩展一个flatMap方法。

    fun <A, B> Kind<F, A>.flatMap(f: (A) -> Kind<F, B>): Kind<F, B>

有了flatMap我们可以写出伪代码

    fun <A, B, C> map2(fa: Kind<F, A>, fb: Kind<F, B>, f: (A, B) -> C): Kind<F, C> {
        fa.flatMap { a =>fb.map(b=>f(a, b) }
    }
}

我们引入一个pure方法,也就是一个unit方法,作用将A类型参数转化为Kind<F,A>类型,map方法同样可以用flatMap实现。

这期是就是Monad。

(4)什么是 Monad

//-------------Monad  pure+flatMap-->map
interface Monad<F> {
    fun <A> pure(a: A): Kind<F, A>
    fun <A, B> Kind<F, A>.flatMap(f: (A) -> Kind<F, B>): Kind<F, B>
}

构建一个Monad的ListMonad 实例

object ListMonad : Monad<List.K> {
    private fun <A> append(fa: Kind<List.K, A>, fb: Kind<List.K, A>): Kind<List.K, A> {
        return if (fa is Cons) {
            Cons(fa.head, append(fa.tail, fb).unwrap())
        } else {
            fb
        }
    }

    override fun <A> pure(a: A): Kind<List.K, A> {
        return Cons(a, Nil)
    }


    override fun <A, B> Kind<List.K, A>.flatMap(f: (A) -> Kind<List.K, B>)
            : Kind<List.K, B> {

        val fa = this
        val empty: Kind<List.K, B> = Nil
        return ListFoldable.run {
            fa.fold(empty)({ r, l ->
                append(r, f(l))
            })
        }
    }

}

5. Applicative 重新定义Monad

用pure和flatMap实现map。那么所有的Monad其实就是Functor,定义Monad<F>时候,直接实现Functor<F>

我们定义一个具体Applicative<F>

interface Functor<F> {
    fun <A, B> Kind<F, A>.map(f: (A) -> B): Kind<F, B>
}

//Applicative 结构
interface Applicative<F> : Functor<F> {
    fun <A> pure(a: A): Kind<F, A>
    fun <A, B> Kind<F, A>.ap(f: Kind<F, (A) -> B>): Kind<F, B>
    override fun <A, B> Kind<F, A>.map(f: (A) -> B): Kind<F, B> {
        return ap(pure(f))
    }
}

Applicative<F> 直接实现Functor<F>,在内部高阶类型扩展ap方法,ap接受一个高阶类型,Kind<F,(A) -> (B)>参数然后返回Kind<A,B>

//重新定义Monad<F> 及时Applicative<F> 也是 Functor<F> 同时定义了map和ap方法
interface Monad2<F> : Applicative<F> {
    fun <A, B> Kind<F, A>.flatMap(f: (A) -> Kind<F, B>): Kind<F, B>
    override fun <A, B> Kind<F, A>.ap(f: Kind<F, (A) -> B>): Kind<F, B> {
        return f.flatMap { fn ->
            this.flatMap { a ->
                pure(fn(a))
            }
        }
    }
}

Monad 组合副作用

最常见IO操作,创建一个代表输入输出类型StdIO<A>,实现Kind<StdIO.A>

//----------Monad 副作用组合

sealed class StdIO<A> : Kind<StdIO.K, A> {
    object K
    companion object {
        fun read(): StdIO<String> {
            return ReadLine
        }

        fun write(l: String): StdIO<Unit> {
            return WriteLine(l)
        }

        fun <A> pure(a: A): StdIO<A> {
            return Pure(a)
        }
    }
}

object ReadLine : StdIO<String>()
data class WriteLine(val line: String) : StdIO<Unit>()
data class Pure<A>(val a: A) : StdIO<A>()

我们创建单利对象ReadLine,数据类WriteLine读写操作,以及Pure类接受A类型参数,表示StdIO<A>实例。我在其中半生对象实现read,write,pure。我们实现StdIOMonad

inline fun <A> Kind<StdIO.K, A>.unwrap(): StdIO<A> = this as StdIO<A>

//StdIOMonad 实现
data class FlatMap<A, B>(val fa: StdIO<A>, val f: (A) -> StdIO<B>) : StdIO<B>()
object StdIOMonad : Monad<StdIO.K> {
    override fun <A> pure(a: A): Kind<StdIO.K, A> {
        return Pure(a)
    }

    override fun <A, B> Kind<StdIO.K, A>.flatMap(f: (A) -> Kind<StdIO.K, B>)
            : Kind<StdIO.K, B> {
        return FlatMap<A, B>(this.unwrap(), ({ a ->
            f(a).unwrap()
        }))
    }
}

StdIOMonad 实现了 Monad<StdIO.K>为Kind<StdIO.K,A>扩展flatMap方法,接着就用StdIO和StdIOMonad实现具体的读写业务例子。

//StdIOMonad 实例,读取两个数字进行加法操作,然后输出结果
fun <A> perform(stdIO: StdIO<A>): A {
    fun <C, D> runFlatMap(fm: FlatMap<C, D>) {
        perform(fm.f(perform(fm.fa)))
    }

    return when (stdIO) {
        is ReadLine -> readLine() as A
        is Pure<A> -> stdIO.a
        is FlatMap<*, A> -> runFlatMap(stdIO) as A
        is WriteLine -> println(stdIO.line) as A
    }
}

val io = StdIOMonad.run {
    StdIO.read().flatMap { a ->
        StdIO.read().flatMap { b ->
            StdIO.write((a.toInt() + b.toInt()).toString())
        }
    }
}

测试调用:

 perform(io.unwrap())
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容