在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
当前回答
这个答案从一个激励性的例子开始,通过这个例子,得出一个单子的例子,并正式定义了“单子”。
考虑伪代码中的这三个函数:
f(<x, messages>) := <x, messages "called f. ">
g(<x, messages>) := <x, messages "called g. ">
wrap(x) := <x, "">
f采用<x,messages>形式的有序对,并返回一个有序对。它保持第一项不变,并在第二项后面附加“called f.”。与g相同。
您可以组合这些函数并获得原始值,以及显示函数调用顺序的字符串:
f(g(wrap(x)))
= f(g(<x, "">))
= f(<x, "called g. ">)
= <x, "called g. called f. ">
您不喜欢f和g负责将自己的日志消息附加到先前的日志信息。(为了论证起见,想象一下,f和g必须对这对中的第二项执行复杂的逻辑,而不是附加字符串。在两个或多个不同的函数中重复这种复杂的逻辑会很痛苦。)
您更喜欢编写更简单的函数:
f(x) := <x, "called f. ">
g(x) := <x, "called g. ">
wrap(x) := <x, "">
但看看当你编写它们时会发生什么:
f(g(wrap(x)))
= f(g(<x, "">))
= f(<<x, "">, "called g. ">)
= <<<x, "">, "called g. ">, "called f. ">
问题是,将一对传递到函数中并不能得到所需的结果。但如果你可以将一对输入到函数中呢:
feed(f, feed(g, wrap(x)))
= feed(f, feed(g, <x, "">))
= feed(f, <x, "called g. ">)
= <x, "called g. called f. ">
将feed(f,m)读为“feed m into f”。要将一对<x,messages>输入函数f,需要将x传递给f,从f中获取<y,messages〕,并返回<y,message message>。
feed(f, <x, messages>) := let <y, message> = f(x)
in <y, messages message>
请注意,当您对函数执行三项操作时会发生什么:
首先:如果包装一个值,然后将结果对送入函数:
feed(f, wrap(x))
= feed(f, <x, "">)
= let <y, message> = f(x)
in <y, "" message>
= let <y, message> = <x, "called f. ">
in <y, "" message>
= <x, "" "called f. ">
= <x, "called f. ">
= f(x)
这与将值传递给函数相同。
第二:如果你把一对放进包装里:
feed(wrap, <x, messages>)
= let <y, message> = wrap(x)
in <y, messages message>
= let <y, message> = <x, "">
in <y, messages message>
= <x, messages "">
= <x, messages>
这不会改变这对。
第三:如果定义了一个函数,该函数将x和g(x)输入f:
h(x) := feed(f, g(x))
并向其中输入一对:
feed(h, <x, messages>)
= let <y, message> = h(x)
in <y, messages message>
= let <y, message> = feed(f, g(x))
in <y, messages message>
= let <y, message> = feed(f, <x, "called g. ">)
in <y, messages message>
= let <y, message> = let <z, msg> = f(x)
in <z, "called g. " msg>
in <y, messages message>
= let <y, message> = let <z, msg> = <x, "called f. ">
in <z, "called g. " msg>
in <y, messages message>
= let <y, message> = <x, "called g. " "called f. ">
in <y, messages message>
= <x, messages "called g. " "called f. ">
= feed(f, <x, messages "called g. ">)
= feed(f, feed(g, <x, messages>))
这与将对输入g和将所得对输入f相同。
你有大部分的单子。现在您只需要了解程序中的数据类型。
<x,“称为f”>是什么类型的值?这取决于x是什么类型的值。如果x是t类型的,那么你的对就是“t和字符串对”类型的值了。称之为M型。
M是一个类型构造器:M本身并不表示一个类型,但一旦你用一个类型填空,M _就表示一个。M int是一对int和一个字符串。M字符串是一对字符串和一个字符串。等
恭喜你,你已经创建了monad!
形式上,你的monad是元组<M,feed,wrap>。
monad是一个元组<M,feed,wrap>,其中:
M是类型构造函数。feed接受一个(函数接受一个t并返回一个M u)和一个M t并返回M u。wrap接受一个v并返回一个M v。
t、 u和v是可以相同也可以不同的任意三种类型。单子满足您为特定单子证明的三个财产:
将包裹的t送入函数与将未包裹的t传入函数相同。形式上:饲料(f,包装(x))=f(x)将M t喂入包装物对M t没有任何影响。形式上:进给(包裹,m)=m将一个M t(称为M)输入一个函数将t传递到g从g得到一个M u(称为n)将n输入f与m进g从g得到n将n输入f形式上:饲料(h,m)=饲料(f,饲料(g,m)),其中h(x):=饲料(f,g(x))
通常,feed称为bind(在Haskell中为AKA>>=),wrap称为return。
其他回答
实际上,monad基本上允许回调嵌套(具有相互递归的线程状态(请忽略连字符))(以可组合(或可分解)的方式)(具有类型安全性(有时(取决于语言))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
例如,这不是单子:
//JavaScript is 'Practical'
var getAllThree =
bind(getFirst, function(first){
return bind(getSecond,function(second){
return bind(getThird, function(third){
var fancyResult = // And now make do fancy
// with first, second,
// and third
return RETURN(fancyResult);
});});});
但是monad启用了这样的代码。monad实际上是一组类型:{bind,RETURN,也许其他我不认识的人…}。这本质上是无关紧要的,实际上是不切实际的。
所以现在我可以使用它:
var fancyResultReferenceOutsideOfMonad =
getAllThree(someKindOfInputAcceptableToOurGetFunctionsButProbablyAString);
//Ignore this please, throwing away types, yay JavaScript:
// RETURN = K
// bind = \getterFn,cb ->
// \in -> let(result,newState) = getterFn(in) in cb(result)(newState)
或将其分解:
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(fancyResult2){
return bind(getThird, function(third){
var fancyResult3 = // And now make do fancy
// with fancyResult2,
// and third
return RETURN(fancyResult3);
});});
或者忽略某些结果:
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(____dontCare____NotGonnaUse____){
return bind(getThird, function(three){
var fancyResult3 = // And now make do fancy
// with `three` only!
return RETURN(fancyResult3);
});});
或者从以下内容简化一个小案例:
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(_){
return bind(getThird, function(three){
return RETURN(three);
});});
收件人(使用“正确身份”):
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(_){
return getThird;
});
或者把它们挤在一起:
var getAllThree =
bind(getFirst, function(first_dontCareNow){
return bind(getSecond,function(second_dontCareNow){
return getThird;
});});
这些能力的实用性并没有真正显现出来,或者变得清晰,直到你试图解决真正的棘手问题例如解析或模块/ajax/资源加载。
你能想象成千上万行indexOf/subString逻辑吗?如果频繁的解析步骤包含在小函数中呢?像字符、空格、大写字符或数字这样的函数?如果这些函数在回调中给出了结果,而不必与Regex集团和争论发生冲突?如果它们的组成/分解被很好地理解了呢?这样你就可以从下往上构建大型解析器了吗?
因此,管理嵌套回调范围的能力非常实用,尤其是在使用monadic解析器组合器库时。(也就是说,根据我的经验)
不要挂断电话:-分类理论-可能是月-莫纳德定律-哈斯克尔- !!!!
在几年前回答了这个问题之后,我相信我可以通过。。。
monad是一种函数组合技术,它使用组合函数bind将某些输入场景的处理具体化,以在组合过程中预处理输入。
在正常合成中,函数compose(>>)用于按顺序将合成的函数应用于其前身的结果。重要的是,所组成的函数需要处理其输入的所有场景。
(x->y)>>(y->z)
这种设计可以通过重组输入来改进,以便更容易地询问相关状态。因此,如果y包含有效性的概念,则值可以变成Mb,例如(is_OK,b),而不是简单的y。
例如,当输入仅可能是一个数字时,而不是返回一个可以尽职尽责地包含数字或不包含数字的字符串,您可以将类型重新构造为bool,以指示元组中存在有效数字和数字,例如bool*float。组合函数现在不再需要解析输入字符串来确定数字是否存在,而只需要检查元组的布尔部分。
(Ma->Mb)>>(Mb->Mc)
在这里,合成与合成一起自然发生,因此每个函数必须单独处理其输入的所有场景,尽管现在这样做要容易得多。
然而,如果我们能够将审讯的工作外化,以应对那些处理场景是常规的情况,那又会怎样呢。例如,如果我们的程序在输入不正常时什么都不做,比如is_OK为false时。如果做到了这一点,那么组合函数就不需要自己处理该场景,从而大大简化了代码并实现了另一个级别的重用。
为了实现这种外部化,我们可以使用bind(>>=)函数来执行组合而不是组合。因此,不是简单地将值从一个函数的输出传递到另一个函数输入,而是检查Ma的M部分,并决定是否以及如何将组合函数应用于a。当然,函数绑定将专门为我们的特定M定义,以便能够检查其结构并执行我们想要的任何类型的应用。尽管如此,a可以是任何东西,因为bind仅在确定应用程序需要时将未检查的a传递给组合函数。此外,组合函数本身也不再需要处理输入结构的M部分,从而简化了它们。因此
(a->Mb)>>=(b->Mc)或更简洁地Mb>>=
简言之,一旦输入被设计为充分暴露某些输入场景,monad就外部化了,从而提供了关于处理这些输入场景的标准行为。这种设计是一种外壳和内容模型,其中外壳包含与组合函数的应用程序相关的数据,并由绑定函数查询,并且仅对绑定函数可用。
因此,单子是三件事:
M外壳,用于保存monad相关信息,实现的绑定函数,用于在将组合函数应用于其在外壳中找到的内容值时使用该外壳信息,以及形式为a->Mb的可组合函数,生成包含单元管理数据的结果。
一般来说,函数的输入比其输出更具限制性,其中可能包括错误条件等;因此,Mb结果结构通常非常有用。例如,当除数为0时,除法运算符不返回数字。
此外,monad可以包括将值a包装成monadic类型Ma的包装函数,以及将一般函数a->b包装成monodic函数a->Mb的包装函数。当然,像bind一样,这样的包装函数是M特有的。例如:
let return a = [a]
let lift f a = return (f a)
绑定函数的设计假定了不可变的数据结构和纯函数,其他事情变得复杂,无法保证。因此,有一元定律:
鉴于
M_
return = (a -> Ma)
f = (a -> Mb)
g = (b -> Mc)
然后
Left Identity : (return a) >>= f === f a
Right Identity : Ma >>= return === Ma
Associative : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)
关联性意味着无论何时应用绑定,绑定都会保留求值顺序。也就是说,在上述关联性的定义中,对f和g的括号化绑定的强制早期评估只会导致期望Ma的函数完成绑定。因此,必须先确定Ma的值,然后才能将其值应用于f,进而将结果应用于g。
[免责声明:我仍在努力完全了解monads。以下是我目前所了解的情况。如果这是错误的,希望有有知识的人会在地毯上给我打电话。]
Arnar写道:
Monads只是一种包装东西的方法,它提供了对包装好的东西进行操作而不展开的方法。
正是这样。想法是这样的:
你需要一些价值,并用一些附加信息来包装它。就像值是某种类型的(例如整数或字符串)一样,附加信息也是某种类型的。例如,该额外信息可能是“可能”或“IO”。然后,您有一些运算符,允许您在携带附加信息的同时对打包的数据进行操作。这些运算符使用附加信息来决定如何更改包装值上的操作行为。例如,Maybe Int可以是Just Int或Nothing。现在,如果您将Maybe Int添加到Maybe Int,则运算符将检查它们是否都是内部的Just Int,如果是,则将展开Int,将其传递给加法运算符,将生成的Int重新包装为新的Just Int(这是有效的Maybe Int),从而返回Maybe Int。但如果其中一个是内部的Nothing,则该运算符将立即返回Nothing,这也是一个有效的Maybe Int。这样,你可以假装Maybe Ints只是正常的数字,并对它们进行常规运算。如果你得到了一个Nothing,你的方程仍然会产生正确的结果——而不必到处乱检查Nothing。
但这个例子正是Maybe所发生的事情。如果额外的信息是IO,那么将调用为IO定义的特殊运算符,并且在执行添加之前,它可以执行完全不同的操作。(好吧,将两个IO Int加在一起可能是荒谬的——我还不确定。)
基本上,“monad”大致意思是“模式”。但是,您现在有了一种语言构造(语法和所有),可以将新模式声明为程序中的东西,而不是一本充满了非正式解释和专门命名的模式的书。(这里的不精确之处在于所有模式都必须遵循特定的形式,因此monad不像模式那样通用。但我认为这是大多数人都知道和理解的最接近的术语。)
这就是为什么人们觉得单子如此令人困惑:因为它们是一个通用的概念。问是什么使某物成为monad与问是什么让某物成为模式类似。
但是想想在语言中对模式的概念提供语法支持的含义:你不必阅读“四人帮”一书,记住特定模式的构造,只需编写一次代码,以不可知的通用方式实现这个模式,然后就完成了!然后,您可以重用此模式,如Visitor或Strategy或Façade等,只需用它装饰代码中的操作,而无需反复重新实现它!
所以,这就是为什么理解monad的人会发现它们如此有用的原因:这并不是知识势利者以理解为荣的象牙塔概念(好吧,当然也是如此,teehee),而是实际上让代码更简单。
tl;博士
{-# LANGUAGE InstanceSigs #-}
newtype Id t = Id t
instance Monad Id where
return :: t -> Id t
return = Id
(=<<) :: (a -> Id b) -> Id a -> Id b
f =<< (Id x) = f x
开场白
应用程序运算符$of函数
forall a b. a -> b
是规范定义的
($) :: (a -> b) -> a -> b
f $ x = f x
infixr 0 $
根据Haskell基函数应用f x(infixl 10)。
作文定义为$as
(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \ x -> f $ g x
infixr 9 .
并且满足所有f g h的等价性。
f . id = f :: c -> d Right identity
id . g = g :: b -> c Left identity
(f . g) . h = f . (g . h) :: a -> d Associativity
.是关联的,id是它的右标识和左标识。
克莱斯利三人组
在编程中,monad是带有monad类型类实例的函子类型构造函数。定义和实现有几个等价的变体,每个变体对monad抽象的直觉略有不同。
函子是带有函子类型类实例的*->*类型的类型构造函数f。
{-# LANGUAGE KindSignatures #-}
class Functor (f :: * -> *) where
map :: (a -> b) -> (f a -> f b)
除了遵循静态强制类型协议之外,函子类型类的实例必须遵守所有f g的代数函子定律。
map id = id :: f t -> f t Identity
map f . map g = map (f . g) :: f a -> f c Composition / short cut fusion
函数计算具有以下类型
forall f t. Functor f => f t
计算c r包含上下文c中的结果r。
一元一元函数或Kleisli箭头的类型为
forall m a b. Functor m => a -> m b
Kleisi箭头是接受一个参数a并返回一元计算m b的函数。
Monads是用Kleisli三重函数定义的
(m, return, (=<<))
实现为类型类
class Functor m => Monad m where
return :: t -> m t
(=<<) :: (a -> m b) -> m a -> m b
infixr 1 =<<
Kleisli标识返回是一个Kleisli箭头,它将值t提升为单元上下文m。
Kleisli组成<=<根据扩展定义为
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
f <=< g = \ x -> f =<< g x
infixr 1 <=<
<=<组成两个Kleisli箭头,将左箭头应用于右箭头应用的结果。
monad类型类的实例必须遵守monad定律,这在Kleisli组合中最为优雅地表述为:forall f g h。
f <=< return = f :: c -> m d Right identity
return <=< g = g :: b -> m c Left identity
(f <=< g) <=< h = f <=< (g <=< h) :: a -> m d Associativity
<=<是关联的,返回是它的右标识和左标识。
身份
标识类型
type Id t = t
是类型上的标识函数
Id :: * -> *
被解释为函子,
return :: t -> Id t
= id :: t -> t
(=<<) :: (a -> Id b) -> Id a -> Id b
= ($) :: (a -> b) -> a -> b
(<=<) :: (b -> Id c) -> (a -> Id b) -> (a -> Id c)
= (.) :: (b -> c) -> (a -> b) -> (a -> c)
在规范的Haskell中,定义了身份monad
newtype Id t = Id t
instance Functor Id where
map :: (a -> b) -> Id a -> Id b
map f (Id x) = Id (f x)
instance Monad Id where
return :: t -> Id t
return = Id
(=<<) :: (a -> Id b) -> Id a -> Id b
f =<< (Id x) = f x
选项
选项类型
data Maybe t = Nothing | Just t
编码计算可能t不一定产生结果t,计算可能“失败”。选项monad已定义
instance Functor Maybe where
map :: (a -> b) -> (Maybe a -> Maybe b)
map f (Just x) = Just (f x)
map _ Nothing = Nothing
instance Monad Maybe where
return :: t -> Maybe t
return = Just
(=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b
f =<< (Just x) = f x
_ =<< Nothing = Nothing
a->Maybe b仅在Maybe a产生结果时应用于结果。
newtype Nat = Nat Int
自然数可以编码为大于或等于零的整数。
toNat :: Int -> Maybe Nat
toNat i | i >= 0 = Just (Nat i)
| otherwise = Nothing
自然数在减法下不是封闭的。
(-?) :: Nat -> Nat -> Maybe Nat
(Nat n) -? (Nat m) = toNat (n - m)
infixl 6 -?
选项monad涵盖了异常处理的基本形式。
(-? 20) <=< toNat :: Int -> Maybe Nat
List
列表monad,覆盖列表类型
data [] t = [] | t : [t]
infixr 5 :
及其加法幺半群运算“append”
(++) :: [t] -> [t] -> [t]
(x : xs) ++ ys = x : xs ++ ys
[] ++ ys = ys
infixr 5 ++
编码非线性计算[t],产生自然量0,1。。。结果t。
instance Functor [] where
map :: (a -> b) -> ([a] -> [b])
map f (x : xs) = f x : map f xs
map _ [] = []
instance Monad [] where
return :: t -> [t]
return = (: [])
(=<<) :: (a -> [b]) -> [a] -> [b]
f =<< (x : xs) = f x ++ (f =<< xs)
_ =<< [] = []
Extension=<<将从Kleisli箭头a->[b]的应用f x到[a]的元素的所有列表[b]连接到一个结果列表[b]。
设正整数n的正除数为
divisors :: Integral t => t -> [t]
divisors n = filter (`divides` n) [2 .. n - 1]
divides :: Integral t => t -> t -> Bool
(`divides` n) = (== 0) . (n `rem`)
then
forall n. let { f = f <=< divisors } in f n = []
在定义monad类型类时,Haskell标准使用其flip,即绑定运算符>>=,而不是extension=<<。
class Applicative m => Monad m where
(>>=) :: forall a b. m a -> (a -> m b) -> m b
(>>) :: forall a b. m a -> m b -> m b
m >> k = m >>= \ _ -> k
{-# INLINE (>>) #-}
return :: a -> m a
return = pure
为了简单起见,本解释使用了类型类层次结构
class Functor f
class Functor m => Monad m
在Haskell中,当前的标准层次结构是
class Functor f
class Functor p => Applicative p
class Applicative m => Monad m
因为不仅每个单子都是函子,而且每个应用格也是函子,每个单子也是应用格。
使用列表monad,命令式伪代码
for a in (1, ..., 10)
for b in (1, ..., 10)
p <- a * b
if even(p)
yield p
大致翻译为do块,
do a <- [1 .. 10]
b <- [1 .. 10]
let p = a * b
guard (even p)
return p
等效的monad理解,
[ p | a <- [1 .. 10], b <- [1 .. 10], let p = a * b, even p ]
和表达式
[1 .. 10] >>= (\ a ->
[1 .. 10] >>= (\ b ->
let p = a * b in
guard (even p) >> -- [ () | even p ] >>
return p
)
)
Do符号和monad理解是嵌套绑定表达式的语法糖。绑定运算符用于一元结果的本地名称绑定。
let x = v in e = (\ x -> e) $ v = v & (\ x -> e)
do { r <- m; c } = (\ r -> c) =<< m = m >>= (\ r -> c)
哪里
(&) :: a -> (a -> b) -> b
(&) = flip ($)
infixl 0 &
定义了防护功能
guard :: Additive m => Bool -> m ()
guard True = return ()
guard False = fail
其中单位类型或“空元组”
data () = ()
支持选择和失败的加法单子可以通过使用类型类抽象
class Monad m => Additive m where
fail :: m t
(<|>) :: m t -> m t -> m t
infixl 3 <|>
instance Additive Maybe where
fail = Nothing
Nothing <|> m = m
m <|> _ = m
instance Additive [] where
fail = []
(<|>) = (++)
其中fail和<|>形成所有k l m的幺半群。
k <|> fail = k
fail <|> l = l
(k <|> l) <|> m = k <|> (l <|> m)
失败的是吸收/消灭零元素的加法单体
_ =<< fail = fail
如果在
guard (even p) >> return p
即使p为真,则保护产生[()],并且根据>>的定义,产生局部常数函数
\ _ -> return p
应用于结果()。如果为false,则保护生成列表monad的fail([]),这不会产生要应用>>的Kleisli箭头的结果,因此跳过此p。
状态
不光彩的是,monad用于编码有状态计算。
状态处理器是一种功能
forall st t. st -> (t, st)
转换状态st并产生结果t。状态st可以是任何东西。没有,标志,计数,数组,句柄,机器,世界。
状态处理器的类型通常称为
type State st t = st -> (t, st)
状态处理器monad是kind*->*函子state st.Kleisli状态处理器mond的箭头是函数
forall st a b. a -> (State st) b
在规范的Haskell中,定义了状态处理器monad的惰性版本
newtype State st t = State { stateProc :: st -> (t, st) }
instance Functor (State st) where
map :: (a -> b) -> ((State st) a -> (State st) b)
map f (State p) = State $ \ s0 -> let (x, s1) = p s0
in (f x, s1)
instance Monad (State st) where
return :: t -> (State st) t
return x = State $ \ s -> (x, s)
(=<<) :: (a -> (State st) b) -> (State st) a -> (State st) b
f =<< (State p) = State $ \ s0 -> let (x, s1) = p s0
in stateProc (f x) s1
状态处理器通过提供初始状态来运行:
run :: State st t -> st -> (t, st)
run = stateProc
eval :: State st t -> st -> t
eval = fst . run
exec :: State st t -> st -> st
exec = snd . run
状态访问由原语get和put提供,它们是对有状态monad的抽象方法:
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}
class Monad m => Stateful m st | m -> st where
get :: m st
put :: st -> m ()
m->st声明状态类型st对monad m的函数依赖性;例如,状态t将确定状态类型为t唯一。
instance Stateful (State st) st where
get :: State st st
get = State $ \ s -> (s, s)
put :: st -> State st ()
put s = State $ \ _ -> ((), s)
单位类型类似于C中的空隙。
modify :: Stateful m st => (st -> st) -> m ()
modify f = do
s <- get
put (f s)
gets :: Stateful m st => (st -> t) -> m t
gets f = do
s <- get
return (f s)
gets通常与记录字段访问器一起使用。
状态monad等价于变量线程
let s0 = 34
s1 = (+ 1) s0
n = (* 12) s1
s2 = (+ 7) s1
in (show n, s2)
其中s0::Int,是同样透明的,但更加优雅和实用
(flip run) 34
(do
modify (+ 1)
n <- gets (* 12)
modify (+ 7)
return (show n)
)
modify(+1)是一种类型为State Int()的计算,但其效果等同于return()。
(flip run) 34
(modify (+ 1) >>
gets (* 12) >>= (\ n ->
modify (+ 7) >>
return (show n)
)
)
结合性的单子定律可以用>>=forall m f g来表示。
(m >>= f) >>= g = m >>= (\ x -> f x >>= g)
or
do { do { do {
r1 <- do { x <- m; r0 <- m;
r0 <- m; = do { = r1 <- f r0;
f r0 r1 <- f x; g r1
}; g r1 }
g r1 }
} }
与面向表达式的编程(例如Rust)一样,块的最后一条语句表示其产量。绑定运算符有时被称为“可编程分号”。
对结构化命令式编程中的迭代控制结构原语进行单点仿真
for :: Monad m => (a -> m b) -> [a] -> m ()
for f = foldr ((>>) . f) (return ())
while :: Monad m => m Bool -> m t -> m ()
while c m = do
b <- c
if b then m >> while c m
else return ()
forever :: Monad m => m t
forever m = m >> forever m
输入/输出
data World
I/O世界状态处理器monad是纯Haskell和真实世界的协调,是功能外延和命令式操作语义的协调。与实际严格执行情况类似:
type IO t = World -> (t, World)
不纯洁的原语促进了交互
getChar :: IO Char
putChar :: Char -> IO ()
readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()
hSetBuffering :: Handle -> BufferMode -> IO ()
hTell :: Handle -> IO Integer
. . . . . .
使用IO原语的代码的杂质由类型系统永久协议化。因为纯净是可怕的,在IO中发生的一切,都留在IO中。
unsafePerformIO :: IO t -> t
或者,至少应该。
Haskell程序的类型签名
main :: IO ()
main = putStrLn "Hello, World!"
扩展到
World -> ((), World)
改变世界的函数。
后记
对象是Haskell类型,态射是Haskelr类型之间的函数的类别是,“快速和松散”,类别是Hask。
函子T是从范畴C到范畴D的映射;对于C中的每个对象,D中的一个对象
Tobj : Obj(C) -> Obj(D)
f :: * -> *
对于C中的每个态射,D中的一个态射
Tmor : HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))
map :: (a -> b) -> (f a -> f b)
其中X,Y是C中的对象。HomC(X,Y)是C中所有态射X->Y的同态类。
Tmor Tobj
T(id) = id : T(X) -> T(X) Identity
T(f) . T(g) = T(f . g) : T(X) -> T(Z) Composition
范畴C的Kleisli范畴由Kleisli三元组给出
<T, eta, _*>
内函子的
T : C -> C
(f) 、同一态射eta(return)和扩展运算符*(=<<)。
Hask中的每个Kleisli态射
f : X -> T(Y)
f :: a -> m b
由扩展运算符
(_)* : Hom(X, T(Y)) -> Hom(T(X), T(Y))
(=<<) :: (a -> m b) -> (m a -> m b)
在Hask的Kleisli范畴中给出了一个态射
f* : T(X) -> T(Y)
(f =<<) :: m a -> m b
Kleisli范畴中的成分。T以扩展的形式给出
f .T g = f* . g : X -> T(Z)
f <=< g = (f =<<) . g :: a -> m c
并且满足范畴公理
eta .T g = g : Y -> T(Z) Left identity
return <=< g = g :: b -> m c
f .T eta = f : Z -> T(U) Right identity
f <=< return = f :: c -> m d
(f .T g) .T h = f .T (g .T h) : X -> T(U) Associativity
(f <=< g) <=< h = f <=< (g <=< h) :: a -> m d
应用等价变换
eta .T g = g
eta* . g = g By definition of .T
eta* . g = id . g forall f. id . f = f
eta* = id forall f g h. f . h = g . h ==> f = g
(f .T g) .T h = f .T (g .T h)
(f* . g)* . h = f* . (g* . h) By definition of .T
(f* . g)* . h = f* . g* . h . is associative
(f* . g)* = f* . g* forall f g h. f . h = g . h ==> f = g
在扩展方面是规范给出的
eta* = id : T(X) -> T(X) Left identity
(return =<<) = id :: m t -> m t
f* . eta = f : Z -> T(U) Right identity
(f =<<) . return = f :: c -> m d
(f* . g)* = f* . g* : T(X) -> T(Z) Associativity
(((f =<<) . g) =<<) = (f =<<) . (g =<<) :: m a -> m c
Monads也可以不使用Kleislian扩展来定义,而是在称为join的编程中使用自然转换mu来定义。一个单元是用μ来定义的,它是一个内函子的范畴C上的三元组
T : C -> C
f :: * -> *
和两种自然变形
eta : Id -> T
return :: t -> f t
mu : T . T -> T
join :: f (f t) -> f t
满足等效条件
mu . T(mu) = mu . mu : T . T . T -> T . T Associativity
join . map join = join . join :: f (f (f t)) -> f t
mu . T(eta) = mu . eta = id : T -> T Identity
join . map return = join . return = id :: f t -> f t
然后定义monad类型类
class Functor m => Monad m where
return :: t -> m t
join :: m (m t) -> m t
选项monad的规范mu实现:
instance Monad Maybe where
return = Just
join (Just m) = m
join Nothing = Nothing
concat函数
concat :: [[a]] -> [a]
concat (x : xs) = x ++ concat xs
concat [] = []
是列表monad的连接。
instance Monad [] where
return :: t -> [t]
return = (: [])
(=<<) :: (a -> [b]) -> ([a] -> [b])
(f =<<) = concat . map f
联接的实现可以使用等价项从扩展形式转换
mu = id* : T . T -> T
join = (id =<<) :: m (m t) -> m t
从mu到扩展形式的反向转换如下
f* = mu . T(f) : T(X) -> T(Y)
(f =<<) = join . map f :: m a -> m b
Philip Wadler:函数编程的MonadsSimon L Peyton Jones,Philip Wadler:强制函数式编程Jonathan M.D.Hill,Keith Clarke:范畴理论、范畴理论单子及其与函数编程的关系简介´Kleisli类别Eugenio Moggi:计算和单子的概念莫纳德不是什么
但为什么如此抽象的理论对编程有用呢?答案很简单:作为计算机科学家,我们重视抽象!当我们设计软件组件的接口时,我们希望它尽可能少地揭示实现。我们希望能够用许多替代方案来替代实现,许多其他“实例”都是相同的“概念”。当我们为许多程序库设计通用接口时,更重要的是我们选择的接口具有多种实现。我们非常重视monad概念的普遍性,这是因为范畴理论非常抽象,所以它的概念对编程非常有用。因此,我们在下面介绍的单子的推广也与范畴理论有着密切的联系,这一点不足为奇。但我们强调,我们的目的非常实用:它不是“实现范畴理论”,而是找到一种更通用的方法来构造组合子库。数学家已经为我们做了很多工作,这是我们的幸运!
从约翰·休斯的《概括单子到箭头》
事实上,与一般人对蒙得斯的理解相反,他们与国家无关。Monads只是一种包装东西的方法,它提供了对包装好的东西进行操作而不展开的方法。
例如,您可以在Haskell中创建一个类型来包装另一个类型:
data Wrapped a = Wrap a
包装我们定义的东西
return :: a -> Wrapped a
return x = Wrap x
要在不展开的情况下执行操作,假设您有一个函数f::a->b,然后您可以执行此操作来提升该函数以作用于包装的值:
fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)
这就是所有需要理解的。然而,事实证明,有一个更通用的函数来执行此提升,即bind:
bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x
bind可以比fmap做得更多,但反之亦然。实际上,fmap只能用绑定和返回来定义。因此,在定义monad时。。您给出它的类型(这里是Wrapped a),然后说明它的返回和绑定操作是如何工作的。
很酷的是,这是一个普遍的模式,它会在所有地方弹出,以纯方式封装状态只是其中之一。
有关如何使用monad来引入函数依赖关系,从而控制求值顺序(如Haskell的IO monad中所用)的好文章,请查看IOInside。
至于理解单子,不要太担心。读一些你觉得有趣的东西,如果你不马上理解,也不要担心。那就用Haskell这样的语言潜水吧。修道院就是这样一种东西,在那里,通过练习,理解慢慢地进入你的大脑,有一天你突然意识到你理解了它们。