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

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


当前回答

Monad是一种带有特殊机器的盒子,它允许你从两个嵌套的盒子中制作一个普通的盒子,但仍然保持两个盒子的一些形状。

具体来说,它允许您执行连接,类型为Monad m=>m(m a)->m a。

它还需要一个返回操作,它只包装一个值。return::Monad m=>a->m a你也可以说joinunboxes和return wrappes,但join不是Monad m=>m a->a类型的(它不会打开所有Monad,而是打开Monad,Monad在其中)

所以它取一个Monad盒子(Monad m=>,m),里面有一个盒子((m a)),然后生成一个普通盒子(m a。

然而,Monad通常用于(>>=)(口语“bind”)运算符,它本质上只是一个fmap和一个接一个的join。具体而言,

x >>= f = join (fmap f x)
(>>=) :: Monad m => (a -> m b) -> m a -> m b

请注意,函数出现在第二个参数中,而不是fmap。

此外,join=(>>=id)。

为什么这有用?本质上,它允许您在某种框架(Monad)中工作时制作将动作串在一起的程序。

Haskell中Monad的最突出用途是IO Monad。现在,IO是对Haskell中的Action进行分类的类型。在这里,Monad系统是唯一的保存方式(华丽的词):

参考透明度懒惰纯洁

本质上,像getLine::IOString这样的IO操作不能被String替换,因为它总是具有不同的类型。把IO想象成一种神奇的盒子,可以把东西传送给你。然而,仍然只是说getLine::IOString和所有函数都接受IOa会导致混乱,因为可能不需要这些函数。const“üp§”getLine会做什么?(const丢弃第二个参数。const a b=a。)getLine不需要求值,但应该执行IO!这使得行为相当不可预测,也使得类型系统不那么“纯粹”,因为所有函数都将采用a和IOa值。

输入IO Monad。

要将动作串在一起,只需展平嵌套的动作。要将函数应用于IO操作的输出,IO a类型中的a,只需使用(>>=)。

例如,输出输入的行(输出行是一个生成IO操作的函数,匹配右参数>>=):

getLine >>= putStrLn :: IO ()
-- putStrLn :: String -> IO ()

这可以用do环境更直观地写出来:

do line <- getLine
   putStrLn line

本质上,这样的do块:

do x <- a
   y <- b
   z <- f x y
   w <- g z
   h x
   k <- h z
   l k w

…转化为:

a     >>= \x ->
b     >>= \y ->
f x y >>= \z ->
g z   >>= \w ->
h x   >>= \_ ->
h z   >>= \k ->
l k w

还有m>>=\_->f的>>运算符(当框中的值不需要在框中创建新框时)也可以写成a>>b=a>>=constb(consta b=a)

此外,返回运算符是根据IO直觉建模的-它返回一个具有最小上下文的值,在这种情况下没有IO。由于IO a中的a表示返回的类型,这类似于命令式编程语言中的return(a),但它不会停止操作链!f>>=return>>=g与f>>=g相同。仅当您返回的术语在链中较早创建时才有用-请参见上文。

当然,还有其他Monad,否则它不会被称为Monad,它会被称为“IO控制”之类的东西。

例如,List Monad(Monad[])通过串联变平-使(>>=)运算符对列表的所有元素执行函数。这可以被视为“不确定性”,其中列表是许多可能的值,而Monad框架正在进行所有可能的组合。

例如(GHCi):

Prelude> [1, 2, 3] >>= replicate 3  -- Simple binding
[1, 1, 1, 2, 2, 2, 3, 3, 3]
Prelude> concat (map (replicate 3) [1, 2, 3])  -- Same operation, more explicit
[1, 1, 1, 2, 2, 2, 3, 3, 3]
Prelude> [1, 2, 3] >> "uq"
"uququq"
Prelude> return 2 :: [Int]
[2]
Prelude> join [[1, 2], [3, 4]]
[1, 2, 3, 4]

因为:

join a = concat a
a >>= f = join (fmap f a)
return a = [a]  -- or "= (:[])"

如果出现这种情况,“也许莫纳德”只会将所有结果作废为“无”。也就是说,绑定自动检查函数(a>>=f)是否返回或值(a>>>=f)是否为Nothing,然后也返回Nothing。

join       Nothing  = Nothing
join (Just Nothing) = Nothing
join (Just x)       = x
a >>= f             = join (fmap f a)

或者更明确地说:

Nothing  >>= _      = Nothing
(Just x) >>= f      = f x

State Monad用于同时修改某些共享状态-s->(a,s)的函数,因此>>=的参数为:a->s->(a,s)。这个名称有点用词不当,因为State实际上是用于状态修改功能,而不是用于状态——状态本身确实没有有趣的财产,它只是被改变了。

例如:

pop ::       [a] -> (a , [a])
pop (h:t) = (h, t)
sPop = state pop   -- The module for State exports no State constructor,
                   -- only a state function

push :: a -> [a] -> ((), [a])
push x l  = ((), x : l)
sPush = state push

swap = do a <- sPop
          b <- sPop
          sPush a
          sPush b

get2 = do a <- sPop
          b <- sPop
          return (a, b)

getswapped = do swap
                get2

那么:

Main*> runState swap [1, 2, 3]
((), [2, 1, 3])
Main*> runState get2 [1, 2, 3]
((1, 2), [1, 2, 3]
Main*> runState (swap >> get2) [1, 2, 3]
((2, 1), [2, 1, 3])
Main*> runState getswapped [1, 2, 3]
((2, 1), [2, 1, 3])

也:

Prelude> runState (return 0) 1
(0, 1)

其他回答

monad是一种具有两个操作的数据类型:>>=(又名bind)和return(又名unit)。return接受一个任意值并用它创建monad的实例。>>=接受monad的一个实例并在其上映射一个函数。(您已经可以看到monad是一种奇怪的数据类型,因为在大多数编程语言中,您无法编写一个接受任意值并从中创建类型的函数。monad使用一种参数多态性。)

在Haskell表示法中,monad接口是

class Monad m where
  return :: a -> m a
  (>>=) :: forall a b . m a -> (a -> m b) -> m b

这些操作应该遵守某些“法则”,但这并不是非常重要的:“法则”只是将操作的合理实现行为化(基本上,>>=和return应该就如何将值转换为monad实例达成一致,并且>>=是关联的)。

Monad不仅仅是关于状态和I/O:它们抽象了一种常见的计算模式,包括处理状态、I/O、异常和非确定性。可能最容易理解的单子是列表和选项类型:

instance Monad [ ] where
    []     >>= k = []
    (x:xs) >>= k = k x ++ (xs >>= k)
    return x     = [x]

instance Monad Maybe where
    Just x  >>= k = k x
    Nothing >>= k = Nothing
    return x      = Just x

其中[]和:是列表构造函数,++是串联运算符,Just和Nothing是Maybe构造函数。这两个monad都在各自的数据类型上封装了常见的有用的计算模式(请注意,两者都与副作用或I/O无关)。

你真的需要写一些非平凡的Haskell代码来理解monad的含义以及它们为什么有用。

http://mikehadlow.blogspot.com/2011/02/monads-in-c-8-video-of-my-ddd9-monad.html

这是你要找的视频。

用C#演示组合和对齐类型的问题,然后用C#正确实现它们。最后,他展示了F#和Haskell中相同的C#代码的外观。

monad是用于封装状态变化的对象的东西。在不允许您具有可修改状态的语言(例如,Haskell)中最常遇到这种情况。

例如文件I/O。

您将能够使用文件I/O的monad来将不断变化的状态本质与使用monad的代码隔离开来。Monad内部的代码可以有效地忽略Monad外部世界的变化状态,这使您更容易理解程序的整体效果。

解释monad似乎就像解释控制流语句一样。想象一下,一个非程序员要求你解释它们?

你可以给他们一个涉及理论的解释——布尔逻辑、寄存器值、指针、堆栈和框架。但那太疯狂了。

你可以用语法来解释它们。基本上,C中的所有控制流语句都有大括号,您可以通过它们相对于括号的位置来区分条件和条件代码。这可能更疯狂。

或者,您也可以解释循环、if语句、例程、子例程以及可能的协例程。

Monad可以取代相当多的编程技术。语言中有一种特定的语法支持它们,还有一些关于它们的理论。

它们也是函数式程序员使用命令式代码而不承认它的一种方式,但这并不是他们唯一的用途。

Monad是一个可应用的(即,你可以将二进制(因此,“n元”)函数提升到(1),并将纯值注入(2))Functor(即,可以映射到(3)的函数,即提升一元函数到(3”),它还具有展平嵌套数据类型的能力(三个概念中的每一个都遵循其相应的一组规则)。在Haskell中,这种扁平化操作称为join。

此“联接”操作的常规(通用、参数化)类型为:

join  ::  Monad m  =>  m (m a)  ->  m a

对于任何monad m(注意,类型中的所有ms都是相同的!)。

特定的m monad定义了其特定版本的join,该版本适用于由类型m A的monadic值“携带”的任何值类型A。某些特定类型包括:

join  ::  [[a]]           -> [a]         -- for lists, or nondeterministic values
join  ::  Maybe (Maybe a) -> Maybe a     -- for Maybe, or optional values
join  ::  IO    (IO    a) -> IO    a     -- for I/O-produced values

连接操作将产生a型值的m计算的m计算转换为a型值组合的m计算。这允许将计算步骤组合成一个更大的计算。

结合“bind”(>>=)运算符的计算步骤简单地使用fmap和join,即。

(ma >>= k)  ==  join (fmap k ma)
{-
  ma        :: m a            -- `m`-computation which produces `a`-type values
  k         ::   a -> m b     --  create new `m`-computation from an `a`-type value
  fmap k ma :: m    ( m b )   -- `m`-computation of `m`-computation of `b`-type values
  (m >>= k) :: m        b     -- `m`-computation which produces `b`-type values
-}

相反,可以通过bind定义join,join mma==join(fmap id mma)==mma>>=id,其中id ma=ma——对于给定的类型m,以更方便的为准。

对于monad,do表示法及其使用代码的等效绑定,

do { x <- mx ; y <- my ; return (f x y) }        --   x :: a   ,   mx :: m a
                                                 --   y :: b   ,   my :: m b
mx >>= (\x ->                                    -- nested
            my >>= (\y ->                        --  lambda
                         return (f x y) ))       --   functions

可以读为

首先“做”mx,当它完成时,将其“结果”作为x,让我用它“做”其他事情。

在给定的do块中,绑定箭头<-右侧的每个值对于某些类型a都是m a类型,在整个do块中都是相同的monad m。

返回x是一个中立的m计算,它只产生给定的纯值x,因此将任何m计算与返回绑定都不会改变该计算。


(1) 提升A2::适用m=>(a->b->c)->m a->m b->m c

(2) 纯::适用m=>a->m a

(3) 具有fmap::函数m=>(a->b)->m a->m b

还有等效的Monad方法,

liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c
return :: Monad m =>  a            -> m a
liftM  :: Monad m => (a -> b)      -> m a -> m b

给定monad,其他定义可以如下

pure   a       = return a
fmap   f ma    = do { a <- ma ;            return (f a)   }
liftA2 f ma mb = do { a <- ma ; b <- mb  ; return (f a b) }
(ma >>= k)     = do { a <- ma ; b <- k a ; return  b      }