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

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


当前回答

一个非常简单的答案是:

Monad是一种抽象,它为封装值、计算新的封装值和展开封装值提供了接口。

它们在实践中的方便之处在于,它们提供了一个统一的接口,用于创建建模状态而非状态的数据类型。

必须理解Monad是一种抽象,即用于处理某种数据结构的抽象接口。然后,该接口用于构建具有一元行为的数据类型。

你可以在Ruby中的Monads中找到一个非常好且实用的介绍,第1部分:简介。

其他回答

我也在努力理解单子。这是我的版本:

Monad是关于对重复的事物进行抽象的。首先,monad本身是一个类型化接口(像抽象泛型类),它有两个函数:bind和return,它们定义了签名。然后,我们可以基于抽象的monad创建具体的monad,当然还有绑定和返回的具体实现。此外,绑定和返回必须满足几个不变量,以便可以组合/链接具体的单体。

当我们有接口、类型、类和其他工具来创建抽象时,为什么要创建monad概念?因为monad提供了更多:它们以一种能够在没有任何样板的情况下合成数据的方式强制重新思考问题。

除了上面出色的答案之外,让我为您提供以下文章的链接(由Patrick Thomson撰写),该文章通过将概念与JavaScript库jQuery(及其使用“方法链接”来操作DOM的方式)相关联来解释monads:jQuery是Monad

jQuery文档本身并没有提到术语“monad”,而是谈到了可能更熟悉的“构建器模式”。这并不能改变一个事实,那就是你有一个合适的monad,也许你甚至没有意识到它。

但是,你本可以发明蒙纳斯!

sigfpe说:但所有这些都将单子介绍为需要解释的深奥的东西。但我想说的是,它们一点都不深奥。事实上,面对函数式编程中的各种问题,你会不可避免地被引向某些解决方案,所有这些都是单子的例子。事实上,如果你还没有发明,我希望你现在就发明它们。这是注意到所有这些解决方案实际上都是变相的相同解决方案的一小步。读完这篇文章后,你可能会更好地理解单子上的其他文档,因为你会发现你所看到的一切都是你已经发明的。monads试图解决的许多问题都与副作用有关。因此,我们将从它们开始。(请注意,monad让您做的不仅仅是处理副作用,特别是许多类型的容器对象都可以被视为monad。monad的一些介绍发现,很难协调monad的这两种不同用法,并且只关注其中一种。)在命令式编程语言(如C++)中,函数的行为与数学函数完全不同。例如,假设我们有一个C++函数,它接受一个浮点参数并返回一个浮点结果。从表面上看,它可能有点像一个将实数映射到实数的数学函数,但C++函数可以做的不仅仅是返回一个依赖于其参数的数字。它可以读取和写入全局变量的值,也可以将输出写入屏幕并接收用户的输入。然而,在纯函数语言中,函数只能读取在其参数中提供给它的内容,而它对世界产生影响的唯一方式是通过它返回的值。

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是函数组合运算符的一种自定义实现,它考虑了副作用以及不兼容的输入和返回值(用于链接)。