关于haskell:一种-Monad-的偏门的理解方式

9次阅读

共计 2862 个字符,预计需要花费 8 分钟才能阅读完成。

我对数学概念属性符号把握得不好, 所以了解比较慢,
这篇文章是从概念性的内容去了解 Monad, 不准确, 可能具体到数学概念也不精确.
然而心愿提供一个比拟直观的形式去理解, Monad 是怎么来的?

概念上简略说 Monad 是 ” 自函子领域上的一个幺半群 ”.
新概念很多, 函子, 自函子, 领域, 半群, 幺半群.
模糊地讲, 这些就是数学的概念, 汇合啦, 汇合元素间的映射啦, 单位元啦,

Monad 概念

含糊了解的话, 函子能够当做是函数, a -> b 的映射, 当然也能够比函数更形象点的货色,
而后 ” 自函子 ”, 波及到类型的概念, 函子从一个汇合 A 到另一个汇合 B,
但咱们把程序所有货色都放在一起的话, 函子能够认为是从 A 到 A 本人了, 所以是 ” 自函子 ”.
“ 领域 ” 我解释不来, 大抵是这些函子的形成和关系, 具体看何幻的文章.
从后面这部分看, Haskell 把程序当成各种汇合还有映射来对待了,
程序中, 无论值的变动, 甚至副作用的变动, 全都纳入到领域里边来了解.

而后幺半群呢? 要了解这些概念, 就要晓得相干的几个概念,

  • 原群(Magma)

一个汇合, 而后汇合上的元素的二元操作, 操作后果都在这个汇合外部,
一个例子, 比方 {true false}, 还有二元操作 and or,
任何二元操作的后果都在汇合内.

  • 半群(Semigroup)

半群在原群的根底上减少了一个条件, 满足结合律:
比方所有非空的字符串的汇合, 以及 concat 操作.
"a" `concat` "b" 失去 "ab" 还在汇合内,
而后 ("a" `concat` "b") `concat` "c" 失去 "abc",
而后 "a" `concat` ("b" `concat` "c") 失去 "abc",
两者是等价的, 满足结合律.

  • 幺半群(Monoid)

幺半群, 在半群的根底上减少一个条件, 存在幺元,
幺元跟任何元素 a 联合失去的都是 a 自身,
例子的话, 在下面这个非空字符串的例子当中再加上空字符串,
那么 ""`concat`"a" 失去 "a",
而后 "a"`concat`"" 失去 "a",
两者等价的, "" 就是这个汇合的幺元.

  • 群(Group)

群在幺半群的根底上在加上了一个条件, 存在逆元,
一个例子比方整数的汇合, 其中
(x + y) + z = x + (y + z) 满足结合律,
x + 0 = 0 + x 存在幺元,
x + (-x) = 0 存在逆元,
所以整数的汇合就是一个幺半群了.

当然这个叙述就不准确了, 但你大抵应该晓得幺半群对应什么了,
汇合, 二元操作闭合, 交换律, 幺元.
特地是字符串这个例子, 能够看到程序当中显著会有很多这样的对应,
咱们大量应用数组, 数组也是满足这几个条件的,
还是那个 concat 操作, 闭合, 交换律, 幺元 ([]), 都是成立,
而后数值计算, +, * 这两个操作, 闭合, 交换律, 幺元, 也是存在的.

而后须要绕过来了解一下了, 对于函数, 对于副作用, 是不是也是幺半群?

函数吧, 有 f g h 三个函数, 而后有个复合函数的操作 compose,
(f `compose` g) `compose` h 是一个,
f `compose` (g `compose` h) 是另一个,
compose 是左边函数先计算的, 所以总是当初 h(x) 先计算, 最终是 f(g(h(x))).
这个是结合律.
能够直观了解一下, 函数联合常识联合的函数, 最终调用还是固定的程序的.
另外幺元就是 function(x){return x} 这个函数了. 左右都一样.

而后副作用呢, 咱们把副作用拿函数包一下, function(){ someEffect() },
而后依照函数 compose 的操作, 还是有结合律的,
而后咱们定义一个副作用为空的 Unit 操作, 能够作为幺元,
因为是空的副作用, 所以放在右边放在左边都是不影响的.

所以函数, 副作用, 这些也还是能依照幺半群来了解和束缚的.
这样想上来, 程序里边大量的内容都是能够套用幺半群去了解了.
而要害地位就是结合律还有幺元, 对于函数来说就是函数复合还有 (x) => x 函数.

既然幺半群是程序当中普遍存在的构造, 也就能间接地承受这个概念了.
而后 ” 自函子领域上的幺半群 ”, 也就是说限定在 ” 自函子 ” 而不是 ” 一般汇合元素 ” 的幺半群了.
这个不能精确形容, 然而应该足够提供一些观点上的了解了, Monad 是怎么进去的 …

Monad class

在 Haskell 里定义又要再另外了解了, 首先对幺半群来说还是清晰的,
而交换律作为 law 没有写在 class 的定义当中了,

class Monoid m where  
    -- 定义元素
    mempty :: m
    
    -- 定义闭合二元操作
    mappend :: m -> m -> m
    
    -- 定义多个元素的联合, 默认是用的 List 示意
    -- 满足结合律, 所以 foldr 是从左边开始联合的, 跟右边联合成果统一
    mconcat :: [m] -> m
    mconcat = foldr mappend mempty

对应的 Monad 版本, 也有着跟 Monoid 对应的一些函数的构造,

class Monad m where
    -- 定义幺元
    return :: a -> m a
    
    -- 对应上边的二元操作 mappend
    (>>=) :: m a -> (a -> m b) -> m b
    
    (>>) :: m a -> m b -> m b
    x >> y = x >>= \_ -> y
    
    -- 对应上边的多个元素的组合 mconcat
    join :: m => m (m a) -> m a

这边容易一致的用法呈现了, 首先幺元的定义:

-- 在 Monoid 当中是
mappend :: m

-- 在 Monad 当中是
return :: a -> m a
-- 或者有的中央用是 pure 的名称
pure :: a -> m a
-- 那么, Monoid 中的二元操作
mappend :: m -> m -> m

-- 到了 Monad, 应该是
mappend :: m a -> m a -> m a

不过咱们理论看到的是两个类型变量, a b:

-- 蕴含从 a 到 m b 态射的一个过程
(>>=) :: m a -> (a -> m b) -> m b
    
-- 蕴含 a 也蕴含 b 然而 a 被抛弃的过程, 比拟可能是通过副作用抛弃了
(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y

这个认真想想也能够了解, 比方 List Int, 整数的数组,
通过映射之后, 可能还是 List Int, 也可能通过 length 失去 Int,
不过在 m a 这个类型束缚当中, 不会是纯的 Int 了,
可能是 List String, 比方 [1,2,3,4].map(JSON.stringify),
可能是 List Boolean, 比方 [1,2,3,4].map(x => x > 0),
总之这个中央因为态射的存在而变动了.

至于为什么要这样定义, 如果说 a -> m b 这个过程不须要跟 m 产生作用,
那么咱们用态射, 间接用 Functor 就能达成了,

class Functor f where 
    fmap :: (a -> b) -> f a -> f b

然而存在状况, 就是须要跟 m 产生作用的, 比方 IO, 就必然会,
而后是 flatMap 的状况, 计算过程要跟 List 这个结构器产生作用:

Prelude> (>>=) [1,2,3,4] (\x -> [0..x])
[0,1,0,1,2,0,1,2,3,0,1,2,3,4]

IO Monad 的特殊性在于支流语言习惯性不去管 IO,
然而依照 Monoid 这套了解下来, IO 的确用是这样的构造.

其余

里边的概念都太形象了, 特地是领域相干的, 这个写得不太能自圆其说.

正文完
 0