函数式通用结构设计
介绍一个非常让人恶心的专业术语,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 {
fun zero(): A
fun A.append(b: A): A
}
我们Monoid做什么,举个小例子,字符串拼接操作
object stringConcatMonoid : Monoid {
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 : Kind {
object K
}
object Nil : List()
data class Cons(val head: A, val tail: List) : List()
扩展一个sum方法,支持指定的一种二元操作,对列表元素操作。和上个文章说的ListFodable,这也是一个典型的fold操作
interface Foldable {
fun Kind.fold(init: B): ((B, A) -> B) -> B
}
object ListFoldable : Foldable {
override fun Kind.fold(init: B): ((B, A) -> B) -> B = { f ->
fun fold0(l: List, v: B): B {
return when (l) {
is Cons -> {
fold0(l.tail, f(v, l.head))
}
else -> v
}
}
fold0(this.unwrap(), init)
}
}
查看前“Kotlin(十七)函数式编程<2>” 相关内容
fun List.sum(ma: Monoid): A {
val fa = this
return ListFoldable.run {
fa.fold(ma.zero())({ s, i ->
ma.run {
s.append(i)
}
})
}
}
sum方法接受Monoid 类型参数ma。Monoid抽象结构非常适合fold操作。回顾下Kotlin里面fold在标准库定义
public inline fun Iterable.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
(1) 函子定律
定义类型Kind
// 模拟高阶类型
interface Kind
interface Functor {
fun Kind.map(f: (A) -> B): Kind
}
这里的类型List.K 替代F,代表一个列表容器,实际上F可以是其他的类型构造器,如
- Kint
可空或者存在的高阶类型 - Kind
拥有副作用的高阶类型 - Kind
代表解析器的高阶类型
object ParserFunctor:Functor{
override def fun Kind.map(f:(A) -> B):Kind
...
}
同一律法则
。
假设有一个identify函数,接受A类型参数,返回结果还是a
fun identify(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
fun Kind.map(f: (A) -> B): Kind
新的管道规格保持不变,旧的容器依旧保持不变,利用递归思想(类似Pair构建出List),类似贪吃蛇可以创造出无尽的列表,用函数支持
fun map2(fa: Kind, fb: Kind, f: (A, B) -> C): Kind
实际业务副作用不可避免,如果我们把副作用限制在管道容器内,管道看做一个拥有原子性整体,那么依旧符合引用透明性。我们可以将相同容器内的副作用利用函数f组合,尽量推迟到最后执行,就是典型函数式编程。
(3)flatMap 实现复杂的组合
map2操作,会得到一个嵌套容器的结构。Kind
Kotlin 支持flatten操作的flatMap可以看成map与flatten的结合操作。可行思路就是给我们之前的高阶类型扩展一个flatMap方法。
fun Kind.flatMap(f: (A) -> Kind): Kind
有了flatMap我们可以写出伪代码
fun map2(fa: Kind, fb: Kind, f: (A, B) -> C): Kind {
fa.flatMap { a =>fb.map(b=>f(a, b) }
}
}
我们引入一个pure方法,也就是一个unit方法,作用将A类型参数转化为Kind
这期是就是Monad。
(4)什么是 Monad
//-------------Monad pure+flatMap-->map
interface Monad {
fun pure(a: A): Kind
fun Kind.flatMap(f: (A) -> Kind): Kind
}
构建一个Monad的ListMonad 实例
object ListMonad : Monad {
private fun append(fa: Kind, fb: Kind): Kind {
return if (fa is Cons) {
Cons(fa.head, append(fa.tail, fb).unwrap())
} else {
fb
}
}
override fun pure(a: A): Kind {
return Cons(a, Nil)
}
override fun Kind.flatMap(f: (A) -> Kind)
: Kind {
val fa = this
val empty: Kind = Nil
return ListFoldable.run {
fa.fold(empty)({ r, l ->
append(r, f(l))
})
}
}
}
5. Applicative 重新定义Monad
用pure和flatMap实现map。那么所有的Monad其实就是Functor,定义Monad
我们定义一个具体Applicative
interface Functor {
fun Kind.map(f: (A) -> B): Kind
}
//Applicative 结构
interface Applicative : Functor {
fun pure(a: A): Kind
fun Kind.ap(f: Kind B>): Kind
override fun Kind.map(f: (A) -> B): Kind {
return ap(pure(f))
}
}
Applicative
//重新定义Monad 及时Applicative 也是 Functor 同时定义了map和ap方法
interface Monad2 : Applicative {
fun Kind.flatMap(f: (A) -> Kind): Kind
override fun Kind.ap(f: Kind B>): Kind {
return f.flatMap { fn ->
this.flatMap { a ->
pure(fn(a))
}
}
}
}
Monad 组合副作用
最常见IO操作,创建一个代表输入输出类型StdIO,实现Kind
//----------Monad 副作用组合
sealed class StdIO : Kind {
object K
companion object {
fun read(): StdIO {
return ReadLine
}
fun write(l: String): StdIO {
return WriteLine(l)
}
fun pure(a: A): StdIO {
return Pure(a)
}
}
}
object ReadLine : StdIO()
data class WriteLine(val line: String) : StdIO()
data class Pure(val a: A) : StdIO()
我们创建单利对象ReadLine,数据类WriteLine读写操作,以及Pure类接受A类型参数,表示StdIO实例。我在其中半生对象实现read,write,pure。我们实现StdIOMonad
inline fun Kind.unwrap(): StdIO = this as StdIO
//StdIOMonad 实现
data class FlatMap(val fa: StdIO, val f: (A) -> StdIO) : StdIO()
object StdIOMonad : Monad {
override fun pure(a: A): Kind {
return Pure(a)
}
override fun Kind.flatMap(f: (A) -> Kind)
: Kind {
return FlatMap(this.unwrap(), ({ a ->
f(a).unwrap()
}))
}
}
StdIOMonad 实现了 Monad
//StdIOMonad 实例,读取两个数字进行加法操作,然后输出结果
fun perform(stdIO: StdIO): A {
fun runFlatMap(fm: FlatMap) {
perform(fm.f(perform(fm.fa)))
}
return when (stdIO) {
is ReadLine -> readLine() as A
is Pure -> 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())