在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?

我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。


当前回答

经过努力,我想我终于明白了单子。在重新阅读了我自己对绝大多数投票结果的冗长批评之后,我将给出这个解释。

要理解单子,需要回答三个问题:

你为什么需要蒙纳德?什么是单子?如何实现monad?

正如我在最初的评论中所指出的,有太多的monad解释被第3个问题所困扰,没有,也没有充分地涵盖第2个问题或第1个问题。

你为什么需要蒙纳德?

Haskell等纯函数式语言与C或Java等命令式语言的不同之处在于,纯函数式程序不一定按特定顺序执行,一步一步执行。Haskell程序更类似于一个数学函数,在该函数中,您可以以任意数量的潜在阶数求解“方程”。这带来了许多好处,其中之一是它消除了某些类型的错误的可能性,特别是那些与“状态”相关的错误。

然而,使用这种编程风格,有些问题不是很容易解决的。有些事情,比如控制台编程和文件i/o,需要按照特定的顺序进行,或者需要维护状态。处理这个问题的一种方法是创建一种表示计算状态的对象,以及一系列将状态对象作为输入并返回新修改的状态对象的函数。

因此,让我们创建一个假设的“状态”值,它表示控制台屏幕的状态。这个值是如何构造的并不重要,但假设它是一个字节长度的ascii字符数组,表示屏幕上当前可见的内容,以及一个表示用户输入的最后一行伪代码的数组。我们已经定义了一些接受控制台状态、修改它并返回新控制台状态的函数。

consolestate MyConsole = new consolestate;

因此,要进行控制台编程,但以纯函数的方式,您需要在彼此之间嵌套许多函数调用。

consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");

以这种方式编程保持了“纯”的功能风格,同时强制对控制台的更改按特定顺序进行。但是,我们可能希望像上面的示例一样,一次只执行几个操作。以这种方式嵌套函数将开始变得笨拙。我们想要的是基本上与上面相同的代码,但编写得更像这样:

consolestate FinalConsole = myconsole:
                            print("Hello, what's your name?"):
                            input():
                            print("hello, %inputbuffer%!");

这确实是一种更方便的写法。但我们如何做到这一点呢?

什么是单子?

一旦你定义了一个类型(比如consoleestate),以及一系列专门为该类型操作而设计的函数,你就可以通过定义一个操作符(比如:(bind))将这些东西的整个包变成一个“monad”,该操作符会自动将返回值输入到左边的函数参数中,转换为与特定类型的绑定运算符一起工作的函数。

如何实现monad?

看到其他答案,似乎可以很自由地跳到细节中。

其他回答

最近我一直在以不同的方式思考莫纳斯。我一直认为它们以数学的方式抽象出执行顺序,这使得新类型的多态性成为可能。

如果您使用的是命令式语言,并且您按照顺序编写一些表达式,那么代码始终按照该顺序运行。

在简单的例子中,当你使用monad时,感觉是一样的——你定义了一个按顺序发生的表达式列表。除此之外,根据您使用的monad,您的代码可能会按顺序运行(如IO monad),同时在多个项目上并行运行(如List monad);它可能会中途停止(如Maybe monad)。它可能会在中途暂停以稍后恢复(如Resume monad)),或者它可能会中途倒带以尝试其他选项(如逻辑单声道)。

因为monad是多态的,所以可以根据需要在不同的monad中运行相同的代码。

此外,在某些情况下,可以将monad组合在一起(使用monad转换器)以同时获得多个特性。

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概念的普遍性,这是因为范畴理论非常抽象,所以它的概念对编程非常有用。因此,我们在下面介绍的单子的推广也与范畴理论有着密切的联系,这一点不足为奇。但我们强调,我们的目的非常实用:它不是“实现范畴理论”,而是找到一种更通用的方法来构造组合子库。数学家已经为我们做了很多工作,这是我们的幸运!

从约翰·休斯的《概括单子到箭头》

遵循您简短、简洁、实用的指示:

理解monad最简单的方法是在上下文中应用/组合函数。假设你有两个计算,它们都可以看作是两个数学函数f和g。

f取一个String并生成另一个String(取前两个字母)g获取一个String并生成另一个String(大写转换)

因此,在任何语言中,“取前两个字母并将其转换为大写”的转换都会写成g(f(“某个字符串”))。因此,在纯完美函数的世界中,合成只是:先做一件事,然后再做另一件事。

但假设我们生活在一个功能可能失败的世界中。例如:输入字符串可能有一个字符长,因此f将失败。所以在这种情况下

f获取一个String并生成一个String或Nothing。g仅在f未失败时生成字符串。否则,将不生成任何内容

所以现在,g(f(“somestring”))需要一些额外的检查:“计算f,如果它失败,那么g应该返回Nothing,否则计算g”

此思想可应用于任何参数化类型,如下所示:

让Context[Sometype]是Context中Sometype的计算。考虑功能

f: :AnyType->上下文[Sometype]g: :某些类型->上下文[AnyOtherType]

合成g(f())应该读作“compute f。在这个上下文中,做一些额外的计算,然后计算g,如果它在上下文中有意义”

让下面的“{|a|m}”表示一些一元数据。宣传以下内容的数据类型:

        (I got an a!)
          /        
    {| a |m}

函数f知道如何创建monad,只要它有一个a:

       (Hi f! What should I be?)
                      /
(You?. Oh, you'll be /
 that data there.)  /
 /                 /  (I got a b.)
|    --------------      |
|  /                     |
f a                      |
  |--later->       {| b |m}

在这里,我们看到函数f试图评估monad,但遭到了谴责。

(Hmm, how do I get that a?)
 o       (Get lost buddy.
o         Wrong type.)
o       /
f {| a |m}

函数f通过使用>>=找到提取a的方法。

        (Muaahaha. How you 
         like me now!?)       
    (Better.)      \
        |     (Give me that a.)
(Fine, well ok.)    |
         \          |
   {| a |m}   >>=   f

殊不知,monad和>>=勾结在一起。

            (Yah got an a for me?)       
(Yeah, but hey    | 
 listen. I got    |
 something to     |
 tell you first   |
 ...)   \        /
         |      /
   {| a |m}   >>=   f

但他们实际上在谈论什么?嗯,这取决于单子。仅仅抽象地谈论用处有限;你必须对特定的单子有一些经验,才能充实理解。

例如,数据类型Maybe

 data Maybe a = Nothing | Just a

有一个monad实例,其行为如下。。。

其中,如果情况只是

            (Yah what is it?)       
(... hm? Oh,      |
forget about it.  |
Hey a, yr up.)    | 
            \     |
(Evaluation  \    |
time already? \   |
Hows my hair?) |  |
      |       /   |
      |  (It's    |
      |  fine.)  /
      |   /     /    
   {| a |m}   >>=   f

但对于Nothing的情况

        (Yah what is it?)       
(... There      |
is no a. )      |
  |        (No a?)
(No a.)         |
  |        (Ok, I'll deal
  |         with this.)
   \            |
    \      (Hey f, get lost.) 
     \          |   ( Where's my a? 
      \         |     I evaluate a)
       \    (Not any more  |
        \    you don't.    |
         |   We're returning
         |   Nothing.)   /
         |      |       /
         |      |      /
         |      |     /
   {| a |m}   >>=   f      (I got a b.)
                    |  (This is   \
                    |   such a     \
                    |   sham.) o o  \
                    |               o|
                    |--later-> {| b |m}

因此,如果Maye monad实际上包含它所宣传的a,则它允许计算继续,但如果不包含,则中止计算。然而,结果仍然是一段单元数据,尽管不是f的输出。因此,Maye monad用于表示失败的上下文。

不同的单子叶植物表现不同。列表是具有一元实例的其他类型的数据。它们的行为如下:

(Ok, here's your a. Well, its
 a bunch of them, actually.)
  |
  |    (Thanks, no problem. Ok
  |     f, here you go, an a.)
  |       |
  |       |        (Thank's. See
  |       |         you later.)
  |  (Whoa. Hold up f,      |
  |   I got another         |
  |   a for you.)           |
  |       |      (What? No, sorry.
  |       |       Can't do it. I 
  |       |       have my hands full
  |       |       with all these "b" 
  |       |       I just made.) 
  |  (I'll hold those,      |
  |   you take this, and   /
  |   come back for more  /
  |   when you're done   / 
  |   and we'll do it   / 
  |   again.)          /
   \      |  ( Uhhh. All right.)
    \     |       /    
     \    \      /
{| a |m}   >>=  f  

在这种情况下,该函数知道如何从其输入生成列表,但不知道如何处理额外的输入和额外的列表。bind>>=,通过组合多个输出帮助f。我通过这个例子来说明,当>>=负责提取a时,它也可以访问f的最终绑定输出。事实上,除非它知道最终输出具有相同类型的上下文,否则它永远不会提取任何a。

还有其他monad用于表示不同的上下文。下面是一些其他特征。IO monad实际上没有a,但它认识一个人,会为你拿到a。州立大学圣莫尼德分校有一个秘密的圣莫尼德,它会把圣莫尼德藏在桌子下面给f,尽管f只是来要求一个a。

所有这一切的关键是,任何类型的数据如果声明自己是Monad,都会声明某种上下文来从Monad中提取值。从这一切中获得的巨大收益?好吧,用某种上下文来进行计算是很容易的。然而,当将多个上下文负载的计算串联在一起时,可能会变得混乱。monad操作负责解决上下文的交互,因此程序员不必这样做。

注意,>>=的使用通过从f中移除一些自主权来缓解混乱。也就是说,例如,在上面的Nothing情况下,f不再能够决定在Nothing的情况下要做什么;它被编码为>>=。这就是权衡。如果f有必要决定在Nothing的情况下做什么,那么f应该是从Maybe a到Maybe b的函数。在这种情况下,也许是monad是无关紧要的。

然而,请注意,有时数据类型不会导出它的构造函数(看看你的IO),如果我们想使用广告值,我们别无选择,只能使用它的monadic接口。

你应该首先了解函子是什么。在此之前,先了解高阶函数。

高阶函数只是一个以函数为自变量的函数。

函子是任何类型构造T,其中存在一个高阶函数,称之为map,它将类型为A->b的函数(给定任意两个类型A和b)转换为函数Ta->Tb。该map函数还必须遵守恒等式和复合法则,以便以下表达式对所有p和q返回true(Haskell表示法):

map id = id
map (p . q) = map p . map q

例如,名为List的类型构造函数是一个函子,如果它配备了一个类型为(a->b)->Lista->Listb的函数,该函数遵守上述定律。唯一实际的实施是显而易见的。生成的Lista->Listb函数在给定列表上迭代,为每个元素调用(a->b)函数,并返回结果列表。

monad本质上只是一个函子T,它有两个额外的方法,类型为T(T A)->T A的join和类型为A->T A的unit(有时称为return、fork或pure)。对于Haskell中的列表:

join :: [[a]] -> [a]
pure :: a -> [a]

为什么有用?因为例如,您可以使用返回列表的函数映射列表。Join获取生成的列表列表并将它们连接起来。列表是monad,因为这是可能的。

您可以编写一个函数,先映射,然后连接。此函数称为bind或flatMap,或(>>=)或(=<<)。这通常是Haskell中给出monad实例的方式。

monad必须满足某些定律,即联接必须是关联的。这意味着,如果您的值x类型为[[a]]],那么join(join x)应该等于join(map joinx)。纯必须是联接的标识,这样联接(纯x)==x。