函数式内功心法-00: 万物互连之monad创世纪
很多人学习haskell,都会在monad这个概念上迷失。真是天下苦monad久矣!
人们常常说 Monad不就是个自函子范畴上的幺半群么?
说这群人简单是丧心病狂,完全不过份。
那么monad到底是什么呢?
为什么说它解决了函数式的副作用?
为什么我们一定要学会monad?
其实一点也不难!
那么让我们开始主题:
- Monad内功心法
a. 欢迎进入Monad
b. Monad是什么?
c. Monad的弟弟Applicative
d: 彻底搞清楚复合连接技术
e: Monad与Applicative的老婆MonadPlus, Alternative
f. Monad的左膀右臂之Functor, Semigroup - Monad能干啥?
a. 为什么需要Monad?
b. 如何用它解决函数式副作用?
c. Monad玩起来 - 源码看透Monad类型特征
a. Functor
b. Semigroup
c. Applicative - 常用Monad类型源码分析
a. State Monad
b. 潘多拉之盒IO Monad
c. maybe monad
d: either monad
e: list monad
f: (->) monad
g: state monad
h: cont monad
e: ... - Monad Transformer库mtl
- Monad家族百花齐放之contraviant, bifunctor, biapplictive, profunctor
- comonad
- free monad
一. Monad内功心法
1. 欢迎进入Monad
在函数式编程世界里,一切都是不可变的。我们没有对象,没有赋值。一切都是全新的世界。
在这全新的世界里面,人们发现了一种技术,它极其简单,却无比强大。
它就是Monad, 一种以函数式为基础而生的复合技术!
想一想小孩子堆积木的玩法,用最简单的积木可以拼出机器人,高楼大厦,千万种变幻。
没错,这就是Monad! 汇小成多,不断复合,却是极其简单!
那么我们接着来进一步揭开monad的面纱.
2. Monad是什么?
Monad是一种类型特征,它的每个类型可以当作一个相互连接的黑盒子。
a. 首先来看什么是类型特征?
我们对于常用的String, Int, Double之类的类型应该比较熟悉了。
对于Int, Double类型来说,它们具有一些共同的特征,比如加减乘除之类的操作,我们就可以将这些特征抽象出来成为类型特征。
b. Monad的类型特征是什么?
Monad的类型特征就是,它是一个黑盒子,它可以连接复合。
- 对于黑盒子,我们记为m a. 显式易见,m是盒子,a是里面的值。
- 对于连接复合,我们记为m a >>= m b,它做什么操作呢?
它吸取盒子a的值连接到盒子b:
m a -> (a -> m b) -> m b
可以把m当作积木块,a则是钩子值,a -> m b则是m a的钩子去连接m b, 最终复合出了新的积木。
3. Monad的弟弟Applicative
后来上面发现,复合技术并不只有Monad一种,还有一种更轻量级的Applicative。
那么我们看看Applicative又是怎么做到的呢?
- 对于Applicative的黑盒子,我们记为f a
- 对于连接复合,我们记为f a <*> f b, 它做什么操作呢?
它穿透盒子f a连接到盒子f b:
f (a -> b) -> f a -> f b
可以把f当作电缆,a则是里面的电流,f(a->b)则是f a的电流穿透去连接f b, 最终连接了两段电缆。
4: 彻底搞清楚复合连接技术
我们以monad哥哥为例, applicative弟弟以此类推。
m a -> (a -> m b) -> m b
我们将m a通过a-> m b钩子传递连接到了m b,中间传递了钩子值a,产生了下一个钩子值b。
当然钩子值b可以继续向下连接复合。
这里面有几个点非常容易想错误:
- a是m a盒子的值!
a不是盒子的值,a是盒子m a传递的值。想得到盒子a的值,就得用钥匙打开它。
并不是所有盒子都能被打开的,所以也就有了后面的潘多拉之盒IO monad
- a是m a盒子的值!
- m a >>= m b的复合连接过程是顺序执行的!
a不是m a盒子的值,m a盒子的值可以部分传递,亦可以完全传递,甚至根据条件传递。
而m b盒子吸收传递值a之后,可以使其前面运行(顺序), 中间运行(嵌入), 尾部运行(逆序),这一切都是可能的。
- m a >>= m b的复合连接过程是顺序执行的!
显示易见, m a >>= m b过程中,我们将传递值a从m a盒子交接到m b盒子后构造出了传递值b。
而m a >>= m b复合连接出的新盒子的实际值需要钥匙打开才能得到。
如果盒子不能打开,我们将得不到它的实际值,但是我们可以使用它的传递值。
在大部分简单情况下,实际值会与传递值相等。
在小部分特殊情况下,我们打不开盒子,特别是潘多拉之盒IO monad。
5: Monad与Applicative的老婆MonadPlus, Alternative
前面讲了复合连接技术的基本形式。
在常用过程中为了使用,就产生了新的两种形式, MonadPlus 与Alternative.
它们是干什么的呢?
对于学过电路的人都会知道,任何复杂的电路,都可以分解为串行与并行。
当然这里的串行与并行是传递顺序,不是执行顺序。
顺序传递很容易理解,如果是顺序执行当然更容易理解了。
并行传递呢? 比如同级菜单,错误条件分支,之类的并行关系就是非常实用的
- 对于Monad,我们有了顺序传递>>=.
Monad顺理成章取了老婆 MonadPlus进行并行传递: m a plus m a.
MonadPlus老婆说: 并行传递合二为一。
并行传递的类型是相同的,所有:
m a -> m a -> m a
*对于弟弟Applicative呢,我们有了穿透传递f a <*> f b
Applicative也顺理成章取了老婆Alternative进行并行传递f a <|> f a
Applicative老婆也不甘示弱,大喊一声: 并行传递合二为一。
f a -> f a -> f a
一家人和和睦睦真是羡慕啊!
6. Monad的左膀右臂之Functor, Segmigroup
Monad的类型特征是可连接复合的黑盒子。它有两大助手,Functor与Segmigroup。
Functor就是黑盒子类型特征,或者人们所熟知的函子。
Semigroup就是复合连接类型特征,或者人们所熟知的半群。
- Functor这个盒子我们定义为f a,它通过fmap 来对黑盒子进行操作:
(a -> b) -> f a -> f b
显而易见,我们对黑盒子f a通过函数a->b 操纵盒子公值a,得到新的公值b,私值依然是要自己打开的。
- Semigroup的连接操作定义了 Semigroup a => a <> a
Semigroup a => a -> a -> a
显而易见,Semigroup类型特征的类型可以通过<>进行连接复合.
二. Monad能干啥?
1. 为什么需要Monad?
在纯函数式的编程世界里面,所有函数都是纯函数,每个值都是不可变的。
所以玩法很不一样,不能用其它的编程方式思考。
函数式编程规则比面向过程,面向对象编程规则简单一百倍,但是它的思维却是强大一百倍!
在haskell编程中,只有一种操作,就是连对盒子进行复合连接。
我们看一下haskell的hello world代码:
main :: IO ()
main = putStrLn "hello monad" >> putStrLn "hello haskell"
haskell的入口就是main函数,main函数就是构造一个IO monad,即main :: IO ()
所以,整个haskell的运行过程就是构建一个IO monad, IO monad实现了monad类型特征。
main :: IO ()就是构建一个IO Monad,它的最终传递值是()。
()是一种存在的空值类型,void也是一种不存在空值类型,Nothing是一种空值数据。
显而易见, main函数就是连接复合出一个io monad,它的最终传递值是我们不需要关心的空值()。
在IO monad的盒子里面,每个连接组件都是IO monad,通过>>=连接。
putStrLn "hello monad" 是一个IO ()
putStrLn "hello haskell"也是一个IO ()
最终通过>>连接起来,>>是>>=的一种方便写法,就是丢弃不使用m a传递的钩子值a
当然我们可以使用do语法宏进行变换,书写起来更加可读.
main :: IO ()
main = do
_ <- putStrLn "hello monad"
putStrLn "hello haskell"
这个就是将>>这种操作的值转换成->这种绑定变量操作,以后我们会经常看到。
是不是很简单,对,我们的编程就只这样一种简单操作:
a >>= (b mplus c mplus d) >>= e
通过最基础最简单的串并边电路逻辑构建出无比美妙的函数式新世界!
这里让我们大喊一声: Hello, Monad!
2. 如何用它解决函数式副作用?
monad以复合技术而生,以解决函数式副作用而闻名!
既然函数值不可变,我们读取文件会引起变化,随机数会变化,我们如何能一个纯函数的世界中生存下来呢?
那我们换一种方法考虑,我们的参数是可以变的,虽然相同的参数得到相同的值,读取不同时刻下面的文件其实是不同的参数。
这个随着环境变化的参数就叫RealWorld,由编译器提供,所以每次运行就产生了不同的值,解决了不可变与副作用的问题。
而只有IO这个潘多拉盒专用处理RealWorld,不能随意打开。
newtype IO a
= GHC.Types.IO (GHC.Prim.State# GHC.Prim.RealWorld
-> (# GHC.Prim.State# GHC.Prim.RealWorld, a #))
所以,我们的整个编程过程其实很简单了。
构建可以处理RealWorld的IO monad盒子,然后复合连接成新的IO monad盒子,通过编译器提供的RealWorld变参,运行不同的IO monad的action,最终实现了不同RealWorld值下对应的副作用实现。
对于每一个IO行为则是一个State Monad,每进行一步处理,读取老折状态 ,产生了新的状态值。