1. FunctorからMonadへ
タイプの観点から、FunctorからApplicative、そしてMonadへは、一般から特別への漸進的なプロセスです(Monadは特別なApplicativeであり、Applicativeは特別なFunctorです)。
Functor
は、通常の関数をコンテキスト付きの値にマップできます
fmap :: (Functor f) => (a -> b) -> f a -> f b
コンテキスト関連の計算で最も単純なシナリオを解決するために使用されます:コンテキストのない関数をコンテキストのある値に適用する方法は?
(+1) ->? Just 1
fmapデビュー:
> fmap (+1) (Just 1)
Just 2
応募可能
Functorの機能強化により、コンテキスト内の関数をコンテキスト付きの値にマップできます
(<*>) :: (Applicative f) => f (a -> b) -> f a -> f b
pure :: (Applicative f) => a -> f a
Applicativeは計算コンテキストとして理解でき、Applicative値は次のような計算です。
おそらく、aは失敗する可能性のある計算を表し、[a]は同時に多くの結果をもたらす計算(非決定論的計算)を表し、IOaは副作用のある計算を表します。
PS計算コンテキストの詳細については、FunctorおよびApplicative_Haskellノート7を参照してください。
コンテキスト関連の計算で別のシナリオを解決するために使用されます:コンテキストを持つ関数をコンテキストを持つ値に適用する方法は?
Just (+1) ->? Just 1
<*>外観:
> Just (+1) <*> (Just 1)
Just2
Monad
はApplicativeで拡張され、入力共通値と出力コンテキスト値を持つ関数をコンテキストを持つ値に適用できます
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
コンテキスト値maがある場合、通常の値aのみを受け入れる関数にそれをスローして、コンテキスト値を返すにはどうすればよいでしょうか。言い換えれば、タイプa-> mbの関数をmaにどのように適用しますか?
コンテキスト関連の計算の最後のシナリオを解決するために使用されます。共通の値を入力し、コンテキストのある値をコンテキストのある値に出力する関数を適用する方法は?
\x -> Just (x + 1) ->? Just 1
=デビュー:
ジャスト1 >> = \ x->ジャスト(x + 1)
ジャスト2
**三者的关联**
从接口行为来看,这三个东西都是围绕具有context的值和函数在搞事情(即,context相关的计算)。那么,考虑一下,共有几种组合情况?
函数输入输出类型一致的情况
* context里的函数 + context里的值:Applicative
* context里的函数 + 普通值:用pure包一下再调
* 普通函数 + context里的值:Functor
* 普通函数 + 普通值:函数调用
函数输入输出类型不一致的情况
* 函数输入普通值,输出context里的值 + context里的值:Monad
* 函数输入普通值,输出context里的值 + 普通值:直接调用
* 函数输入context里的值,输出普通值 + context里的值:直接调用
* 函数输入context里的值,输出普通值 + 普通值:用pure包一下再调
所以,就这个场景(把是否处于context里的函数应用到是否处于context里的值)而言,拥有Functor、Applicative和Monad已经足够应付所有情况了
**二.Monad typeclass**
class Applicative m => Monad m where
(>> =):: forall ab。ma->(a-> mb)-> mb
(>>):: forall ab。ma-> mb-> mb
m >> k = m >> = _- > k
return :: a- > ma
return = pure
fail ::
String- > ma fail s = errorWithoutStackTrace s
实际上,Monad实例只要求实现>>=函数(称之为bind)即可。换言之,Monad就是支持>>=操作的Applicative functor而已
return是pure的别名,所以仍然是接受一个普通值并把它放进一个最小的context中(把普通值包进一个Monad里面)
(>>) :: m a -> m b -> m b定义了默认实现,把函数\_ -> m b通过>>=应用到m a上,用于(链式操作中)忽略前面的计算结果
P.S.链式操作中,把遇到的>>换成>>= \_ ->就很容易弄明白了
P.S.上面类型声明中的forall是指∀(离散数学中的量词,全称量词∀表示“任意”,存在量词∃表示“存在”)。所以forall a b. m a -> (a -> m b) -> m b是说,对于任意的类型变量a和b,>>=函数的类型是m a -> (a -> m b) -> m b。可以省略掉forall a b.,因为默认所有的小写字母类型参数都是任意的:
Haskellでは、小文字の型パラメーターの導入は暗黙的にforallキーワードで始まります
**三.Maybe Monad**
Maybe的Monad实现相当符合直觉:
インスタンスモナドたぶんここで
(ちょうどx)>> = k = kx
何もない>> ==何も
失敗しない=何もない
>>=把函数k应用到Just里的值上,并返回结果,Nothing的话,就直接返回Nothing。例如:
Just 3 >> = \ x-> return(x + 1)
Just 4
Nothing >> = \ x-> return(x + 1)
Nothing
P.S.注意我们提供的函数\x -> return (x + 1),return的价值体现出来了,要求函数类型是a -> m b,所以把结果用return包起来很方便,并且语义也很恰当
这种特性很适合处理一连串可能出错的操作的场景,比如JS的:
const err = error => NaN;
new Promise((resolve、reject)=> {
resolve(1)
})。
then(v => v + 1、err)
.then(v => {throw v}、err)
.then(v => v * 2、err)
.then(console.log.bind(this)、err)
一连串的操作,中间步骤可能出错(throw v),出错后得到表示错误的结果(上例中是NaN),没出错的话就能得到正确的结果
用Maybe的Monad特性来描述:
return 1 >> = \ x-> return(x + 1)>> = _->(fail "NaN" ::たぶんa)>> = \ x-> return(x * 2)
何もありません
1:1完美还原,利用Maybe Monad从容应对一连串可能出错的操作
**四.do表示法**
在I/O场景用到过do语句块(称之为do-notation),可以把一串I/O Action组合起来,例如:
行を行う<-getLine; char <-getChar; return(line ++ [char])
hoho
! "hoho!"
把3个I/O Action串起来,并返回了最后一个I/O Action。实际上,do表示法不仅能用于I/O场景,还适用于任何Monad
就语法而言,do表示法要求每一行都必须是一个monadic value,为什么呢?
因为do表示法只是>>=的语法糖,例如:
foo = do
x <-Just 3
y <-Just "!"
ただ(x ++ yを表示)
类比不涉及context的普通计算:
x = 3とします。y = "!" ショーx ++ y
不难发现do表示法的清爽简洁优势,实际上是:
foo '= Just 3 >> =(\ x->
Just "!" >> =(\ y->
Just(show x ++ y)))
如果没有do表示法,就要手动写一堆lambda嵌套:
Just 3 >> =(\ x-> Just "!" >> =(\ y-> Just(show x ++ y)))
所以<-的作用是:
>> =を使用してモナディック値をラムダにもたらすようなもの
>>=有了,那>>呢,怎么用?
多分Nothing ::多分
IntmaybeNothing = do
start < -return 0
first < -return ((+ 1)start)
Nothing
second < -return ((+ 2)first)
return((+ 3)second)
当我们在do表示法写了一行运算,但没有用到<-来绑定值的话,其实实际上就是用了>>,他会忽略掉计算的结果。我们只是要让他们有序,而不是要他们的结果,而且他比写成_ <- Nothing要来得漂亮的多。
最后,还有fail,do表示法中发生错误时会自动调用fail函数:
fail ::文字列-> ma
fail s = errorWithoutStackTrace s
默认会报错,让程序挂掉,具体Monad实例有自己的实现,比如Maybe:
失敗_ =何もない
忽略错误消息,并返回Nothing。试玩一下:
do(x:xs)<-ただ ""; y <-単に "abc"; yを返す;
何もない
在do语句块中模式匹配失败,直接返回fail,意义在于:
这样模式匹配的失败只会限制在我们monad的context中,而不是整个程序的失败
**五.List Monad**
インスタンスモナド[]ここで
xs >> = f = [y | x <-xs、y <-fx]
(>>)=(*>)
失敗_ = []
List的context指的是一个不确定的环境(non-determinism),即存在多个结果,比如[1, 2]有两个结果(1,2),[1, 2] >>= \x -> [x..x + 2]就有6个结果(1,2,3,2,3,4)
P.S.怎么理解“多个结果”?
初学C语言时有个困惑,函数能不能有多个return?那要怎么返回多个值?
可以返回一个数组(或者结构体、链表等都行),把多个值组织到一起(放进一个数据结构),打包返回
如果一个函数返回个数组,就不确定他返回了多少个结果,这就是所谓的不确定的环境
从List的Monad实现来看,>>=是个映射操作,没什么好说的
>>看起来有点意思,等价于定义在Applicative上的*>:
class Functor f => Applicative f where
(>):: fa-> fb-> fb
a1 > a2 =(id <$ a1)<*> a2
class Functor f where
(<$):: a-> fb-> fa
(<$)= fmap。const
const :: a-> b-> a
const x _ = x
作用是丢弃第一个参数中的值,仅保留结构含义(List长度信息),例如:
> [1, 2] >> [3, 4, 5]
[3,4,5,3,4,5]
等价于:
((fmap。const)id $ [1、2])< > [3、4、5]
[3,4,5,3,4,5]
-または
[id、id] < > [3、4 、5]
[3,4,5,3,4,5]
リストの理解と表記
の興味深い例:[1,2] >> = \ n-> ['a'、 'b'] >> = \ ch-> return(n、ch)
[(1、 'a')、(1、 'b') 、(2、 'a')、(2、 'b')]
最后的n看着不太科学(看infixl 1 >>=好像访问不到),实际上能访问到n,是因为lambda表达式的贪婪匹配特性,相当于:
[1,2] >> = \ n->(['a'、 'b'] >> = \ ch-> return(n、ch))
-括弧で囲まれたフルバージョン
([1、2] >> =(\ n->(['a'、 'b'] >> =(\ ch-> return(n、ch)))))
函数体没界限就匹配到最右端,相关讨论见Haskell Precedence: Lambda and operator
P.S.另外,如果不确定表达式的结合方式(不知道怎么加括号)的话,有神奇的方法,见How to automatically parenthesize arbitrary haskell expressions?
用do表示法重写:
listOfTuples = do
n <-[1,2]
ch <-['a'、 'b']
return(n、ch)
形式上与List Comprehension很像:
[(n、ch)| n <-[1,2]、ch <-['a'、 'b']]
实际上,List Comprehension和do表示法都只是语法糖,最后都会转换成>>=进行计算
**六.Monad laws**
同样,Monad也需要遵循一些规则:
左单位元(Left identity):return a >>= f ≡ f a
右单位元(Right identity):m >>= return ≡ m
结合律(Associativity):(m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)
单位元的性质看起来不很明显,可以借助Kleisli composition转换成更标准的形式:
-| モナドの左から右へのクライスリ構成。
(> =>)::モナドm =>(a-> mb)->(b-> mc)->(a-> mc)
f> => g = \ x-> fx >> = g
(摘自Control.Monad)
从类型声明来看,>=>相当于Monad函数之间的组合运算(monadic function),这些函数输入普通值,输出monadic值。类比普通函数组合:
(。)::( b-> c)->(
a- > b)-> a- > c (。)fg = \ x-> f(gx)
>=>从左向右组合Moand m => a -> m b的函数,.从右向左组合a -> b的函数
P.S.那么,有没有从右向左的Monad函数组合呢?没错,就是<=<
用Kleisli composition(>=>)来描述Monad laws:
左单位元:return >=> f ≡ f
右单位元:f >=> return ≡ f
结合律:(f >=> g) >=> h ≡ f >=> (g >=> h)
满足这3条,所以是标准的Monoid,Moand m => a -> m b函数集合及定义在其上的>=>运算构成幺半群,幺元是return
P.S.用>=>描述的Monad laws,更大的意义在于这3条是形成数学范畴所必须的规律,从此具有范畴的数学意义,具体见Category theory
**MonadPlus**
同时满足Monad和Monoid的东西有专用的名字,叫MonadPlus:
class(Alternative m、Monad m)=> MonadPlus m where
mzero :: ma
mzero = empty
mplus :: ma-> ma-> ma
mplus =(<|>)
在List的场景,mzero就是[],mplus是++:
インスタンスAlternative [] where
empty = []
(<|>)=(++)
这有什么用呢?
比如要对列表元素进行过滤的话,List Comprehension最简单:
[x | x <-[1..50]、 '7'
elem
show x]
[7,17,27,37,47]
用>>=也能搞定:
[1..50] >> = \ x-> if( '7'
elem
show x)then [x] else []
[7,17,27,37,47]
条件表达式看起来有些臃肿,有了MonadPlus就可以换成更简洁有力的表达方式:
[1..50] >> = \ x->ガード( '7'
elem
show x)>> return x
[7,17,27,37,47]
其中guard函数如下:
ガード::(代替f)=>ブール-> f()
ガードTrue =純粋()
ガードFalse =空
输入布尔值,输出具有context的值(True对应放在缺省context里的(),False对应mzero)
经guard处理后,再利用>>把非幺元值恢复成原值(return x),而幺元经过>>运算后还是幺元([]),就被滤掉了
对应的do表示法如下:
SevensOnly = do
x <-[1..50]
guard( '7' elem
show x)
return x
对比List Comprehension形式:
[x | x <-[1..50]、 '7' elem
show x]
非常相像,都是几乎没有多余标点的简练表达
**在do表示法中的作用**
把Monad laws换成do表示法描述的话,就能得到另一组等价转换规则:
-左単位元
do {x '<-return x;
fx '
}
≡do
{fx}
-正しいアイデンティティ
は{x <-m;
戻りX
}
≡
ん{M}
-結合性
do {y <-do {x <-m;
fx
}
gy
}
≡do
{x <-m;
do {y <-fx;
gy
}
}
≡do
{x <-m;
y <-fx;
gy
}
这些规则有2个作用:
能够用来简化代码
skip_and_get = do used
<-getLine
line <-getLine
return line
-冗長なリターンを除去するための使用権のアイデンティティは
=がないskip_and_get
のgetline - <未使用
のgetlineは
ブロックのネストを行う避けることができます
main =
回答を行う<
-skip_and_getputStrLn回答
-展开
メイン=が行う
<答えを-行う
のgetline -未使用<
getlineの
putStrLnの答えを
-連想法則を使用して、ブロックのネストを解除します
main = do
used
< -getLine answer <-getLine
putStrLn answer
**七.Monad与Applicative**
回到最初的场景,我们已经知道了Monad在语法上能够简化context相关计算,能够把a -> m b应用到m a上
既然Monad建立在Applicative的基础之上,那么,与Applicative相比,Monad的核心优势在哪里,凭什么存在?
因为applicative functor并不允许applicative value之间有弹性的交互
这,怎么理解?
再看一个Maybe Applicative的示例:
Just(+1)< >(Just(+2)< >(Just(+3)<*> Just 0))
Just 6
中间环节都不出错的Applicative运算,能够正常得到结果。如果中间环节出错了呢?
-中間障害
Just(+1)< >(Nothing < >(Just(+3)<*> Just 0))
Nothing
也符合预期,纯Applicative运算似乎已经满足需要了。仔细看看刚才是如何表达中间环节的失败的:Nothing <*> some thing。这个Nothing就像是硬编码装上去的炸弹,是个纯静态场景
那想要动态爆炸的话,怎么办?
-柔軟性が不十分
Just(+1)< >(Just(\ x-> if(x> 1)then Nothing return(x + 2))< >(Just(+3)<*> Just 0))
<interactive>:85 :1:エラー:
•制約内の型変数以外の引数:Num(多分a)
(FlexibleContextsを使用してこれを許可します)
•推測された型をチェックするときは
:: foralla。(Ord a、Num(Maybe a)、Num a)=> Maybe(Maybe a)
エラーの理由は、爆発を動的に制御しようとしたことでしたが、多分(多分a)を思いついた:
> Just (\x -> if (x > 1) then Nothing else return (x + 2)) <*> (Just (+3) <*> Just 0)
Just Nothing
この厄介な状況の理由は、Applicativeの<*>が左のコンテキストから機能を機械的に取得し、それを右のコンテキストの値に適用するためです。多分から関数を取得した結果は2つだけです。Nothingから何も取得できず、すぐに爆発するか、Just fからfを取得して、計算からJust(fx)を取得します。前のステップ(x)が爆発しない場合、爆発しません。
したがって、アプリケーションシナリオの観点からは、Monadは一種のコンピューティングコンテキスト制御であり、エラー処理、I / O、不確実な結果の計算など、いくつかの一般的なシナリオを処理できます。その重要性は、アプリケーションよりも柔軟性があり、 Linuxパイプラインのように、計算のすべてのステップで制御を追加します
参考資料
モナド
forallキーワード
モナド法
モナド法の説明