为什么我们需要monad?

时间:2015-01-25 17:27:34

标签: haskell monads

以我的拙见,对着名问题"What is a monad?"的回答,特别是投票最多的问题,试图解释什么是monad而没有明确解释为什么monad真的是必要的。他们可以解释为问题的解决方案吗?

8 个答案:

答案 0 :(得分:540)

为什么我们需要monad?

  1. 我们只想使用功能编程。 ("功能编程(FP)"毕竟)。
  2. 然后,我们遇到了第一个大问题。这是一个程序:

    f(x) = 2 * x

    g(x,y) = x / y

    我们如何说首先执行什么?我们如何使用函数形成一个有序的函数序列(即一个程序

    解决方案:撰写功能。如果您想先g然后f,请写下f(g(x,y))。这样,"程序"也是一个功能:main = f(g(x,y))。好的,但是......

  3. 更多问题:某些功能可能会失败(即g(2,0)除以0)。我们在FP中有 no"例外" (异常不是函数)。我们如何解决它?

    解决方案:让允许函数返回两种类型的东西:而不是让g : Real,Real -> Real(函数从两个实数转换为实数),让我们允许g : Real,Real -> Real | Nothing(从两个实数到(真实或无))。

  4. 但是功能应该(更简单)只返回一件事

    解决方案:让我们创建一种新的数据类型,一个" 拳击类型"包含可能是真实的或者根本就没有。因此,我们可以g : Real,Real -> Maybe Real。好的,但是......

  5. f(g(x,y))现在发生了什么? f尚未准备好使用Maybe Real。而且,我们不想更改我们可以与g关联的所有功能,以使用Maybe Real

    解决方案:让我们的具有特殊功能来连接" /"撰写" /"链接"功能即可。这样,我们可以在幕后调整一个函数的输出来提供下一个函数。

    在我们的案例中:g >>= f(将g连接/撰写为f)。我们希望>>=获得g的输出,检查它,如果Nothing只是不要调用f并返回{{ 1}};或者相反,提取带框的Nothing并用它来提取Real。 (此算法只是f类型的>>=的实现。另请注意,Maybe必须只写一次每个"拳击类型" (不同的盒子,不同的适应算法)。

  6. 可以使用相同的模式解决许多其他问题:1。使用"框"编纂/存储不同的含义/值,并具有>>=之类的函数来返回那些"盒装值"。 2.让作曲家/链接器g帮助将g >>= f的输出连接到g的输入,这样我们就不必更改任何内容完全f

  7. 使用这种技术可以解决的显着问题是:

    • 具有全局状态,即函数序列中的每个函数("程序")都可以共享:solution f

    • 我们不喜欢"不正常的功能":为相同的输入产生不同输出的功能。因此,让我们标记这些函数,使它们返回标记/装箱值:StateMonad monad。

  8. 总幸福!

答案 1 :(得分:209)

答案当然是"我们没有" 。与所有抽象一样,这是不必要的。

Haskell不需要monad抽象。以纯语言执行IO不是必需的。 IO类型本身就可以解决这个问题。现有的do块的monadic desugaring可以替换为bindIO模块中定义的returnIOfailIOGHC.Base。 (它不是关于hackage的文档模块,因此我必须指向its source以获取文档。)所以不,不需要monad抽象。

所以,如果不需要,为什么会存在?因为发现许多计算模式形成了一元结构。抽象结构允许编写适用于该结构的所有实例的代码。更简洁地说 - 代码重用。

在函数式语言中,代码重用的最强大工具是函数的组合。好的(.) :: (b -> c) -> (a -> b) -> (a -> c)运算符非常强大。它可以轻松编写微小的函数并将它们粘合在一起,只需最少的语法或语义开销。

但是有些情况下,这些类型不能很好地解决问题。有foo :: (b -> Maybe c)bar :: (a -> Maybe b)后你会怎么做? foo . bar没有进行类型检查,因为bMaybe b不属于同一类型。

但是......它几乎是对的。你只想要一点余地。您希望能够将Maybe b视为基本上b。但是,将它们视为相同的类型是一个糟糕的主意。这与零指针或多或少相同,Tony Hoare称之为the billion-dollar mistake。因此,如果您不能将它们视为相同的类型,那么您可以找到一种方法来扩展(.)提供的组合机制。

在这种情况下,真正检查(.)背后的理论是非常重要的。幸运的是,有人已经为我们这样做了。事实证明,(.)id的组合形成了一个称为category的数学结构。但是还有其他方法可以形成类别。例如,Kleisli类别允许组合的对象稍微增强。 Maybe的Kleisli类别包含(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)id :: a -> Maybe a。也就是说,类别中的对象使用(->)扩充Maybe,因此(a -> b)变为(a -> Maybe b)

突然间,我们将构图的力量扩展到了传统(.)操作无法解决的问题。这是新抽象力量的来源。 Kleisli类别使用的类型多于Maybe。它们适用于可以组装适当类别的每种类型,遵守类别法则。

  1. 左侧身份:id . f = f
  2. 正确的身份:f . id = f
  3. 相关性:f . (g . h) = (f . g) . h
  4. 只要您能证明您的类型符合这三个法则,您就可以将其转换为Kleisli类别。关于那个最重要的是什么?嗯,事实证明monad与Kleisli类别完全相同。 Monad' return与Kleisli id相同。 Monad (>>=)与Kleisli (.)完全相同,但事实证明,就另一个而言,每个人都很容易。当您在(>>=)(.)之间的差异进行翻译时,类别法律与monad法律相同。

    那么为什么要经历这一切呢?为什么语言中有Monad抽象?正如我在上面提到的,它使代码重用成为可能。它甚至可以在两个不同的维度上重用代码。

    代码重用的第一个维度直接来自抽象的存在。您可以编写适用于抽象所有实例的代码。整个monad-loops包由包含Monad任何实例的循环组成。

    第二个维度是间接的,但它来自组合的存在。当合成很容易时,用小的,可重复使用的块编写代码是很自然的。这与函数的(.)运算符鼓励编写小的可重用函数的方式相同。

    为什么抽象存在?因为它被证明是一种工具,可以在代码中实现更多的组合,从而创建可重用的代码并鼓励创建更多可重用的代码。代码重用是编程的圣杯之一。 monad抽象之所以存在,是因为它让我们向这个圣杯移动了一点。

答案 2 :(得分:24)

本杰明·皮尔斯在TAPL

中说
  

类型系统可以被视为计算一种静态   近似于程序中术语的运行时行为。

这就是为什么配备强大类型系统的语言比表达不良的语言更具表现力。你可以用同样的方式思考monad。

在@Carl和sigfpe点,您可以为数据类型配备所需的所有操作,而无需使用monad,类型类或其他任何抽象的东西。但是monad不仅允许您编写可重用的代码,还可以抽象出所有冗余的细节。

举个例子,假设我们要过滤列表。最简单的方法是使用filter函数:filter (> 3) [1..10],等于[4,5,6,7,8,9,10]

稍微复杂的filter版本,也是从左到右传递累加器,是

swap (x, y) = (y, x)
(.*) = (.) . (.)

filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]

要获得所有ii <= 10, sum [1..i] > 4, sum [1..i] < 25,我们可以写

filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]

等于[3,4,5,6]

或者我们可以使用nub重新定义filterAccum函数,从列表中删除重复元素:

nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []

nub' [1,2,4,5,4,3,1,8,9,4]等于[1,2,4,5,3,8,9]。列表在此处作为累加器传递。代码有效,因为可以保留列表monad,因此整个计算保持纯粹(notElem实际上不使用>>=,但它可以)。但是,无法安全地离开IO monad(即,您无法执行IO操作并返回纯值 - 该值始终将包含在IO monad中)。另一个例子是可变数组:在你离开ST monad之后,一个可变数组存在,你不能再在恒定时间内更新数组。所以我们需要从Control.Monad模块中进行monadic过滤:

filterM          :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ []     =  return []
filterM p (x:xs) =  do
   flg <- p x
   ys  <- filterM p xs
   return (if flg then x:ys else ys)

filterM对列表中的所有元素执行monadic动作,产生元素,monadic动作返回True

带数组的过滤示例:

nub' xs = runST $ do
        arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
        let p i = readArray arr i <* writeArray arr i False
        filterM p xs

main = print $ nub' [1,2,4,5,4,3,1,8,9,4]

按预期打印[1,2,4,5,3,8,9]

带有IO monad的版本,询问要返回的元素:

main = filterM p [1,2,4,5] >>= print where
    p i = putStrLn ("return " ++ show i ++ "?") *> readLn

E.g。

return 1? -- output
True      -- input
return 2?
False
return 4?
False
return 5?
True
[1,5]     -- output

作为最后一个例子,filterAccum可以用filterM来定义:

filterAccum f a xs = evalState (filterM (state . flip f) xs) a

使用StateT monad,它是一种普通的数据类型。

这个例子说明,monad不仅允许你抽象计算上下文并编写干净的可重用代码(由于monad的可组合性,如@Carl所解释的),而且还可以统一处理用户定义的数据类型和内置基元

答案 3 :(得分:18)

我不认为IO应该被认为是一个特别优秀的单子,但对于初学者来说肯定是最令人震惊的单子之一,所以我会用它来解释。

Naïvely为Haskell构建IO系统

纯粹功能语言(实际上是Haskell开始使用的)最简单的可想象的IO系统是这样的:

main₀ :: String -> String
main₀ _ = "Hello World"

有了懒惰,这个简单的签名足以实际构建交互式终端程序 - 非常有限。最令人沮丧的是我们只能输出文字。如果我们添加一些更令人兴奋的输出可能性会怎样?

data Output = TxtOutput String
            | Beep Frequency

main₁ :: String -> [Output]
main₁ _ = [ TxtOutput "Hello World"
          -- , Beep 440  -- for debugging
          ]

可爱,但当然更实际的“替代输出”将是写入文件。但是,您还需要一些方法来文件中读取。有机会吗?

好吧,当我们采用我们的main₁程序并且只是将文件传输到进程(使用操作系统工具)时,我们基本上实现了文件读取。如果我们可以从Haskell语言中触发文件读取......

readFile :: Filepath -> (String -> [Output]) -> [Output]

这将使用“交互程序”String->[Output],将其从文件中获取一个字符串,并生成一个只执行给定程序的非交互式程序。

这里有一个问题:当读取文件时,我们并没有的概念。 [Output]列表确保为输出提供了一个很好的顺序,但我们没有得到输入的订单。

解决方案:将输入事件也作为要执行的事项列表中的项目。

data IO₀ = TxtOut String
         | TxtIn (String -> [Output])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [Output])
         | Beep Double

main₂ :: String -> [IO₀]
main₂ _ = [ FileRead "/dev/null" $ \_ ->
             [TxtOutput "Hello World"]
          ]

好的,现在你可能发现了一个不平衡:你可以读取一个文件并根据它来输出,但你不能使用文件内容来决定例如还读了另一个文件。明显的解决方案:使输入事件的结果也是IO类型的结果,而不仅仅是Output。这确实包括简单的文本输出,但也允许读取其他文件等。

data IO₁ = TxtOut String
         | TxtIn (String -> [IO₁])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [IO₁])
         | Beep Double

main₃ :: String -> [IO₁]
main₃ _ = [ TxtIn $ \_ ->
             [TxtOut "Hello World"]
          ]

现在实际上允许你在程序中表达你可能想要的任何文件操作(尽管可能没有良好的性能),但它有点过于复杂:

  • main₃会生成一个完整的列表操作。为什么我们不使用签名:: IO₁,这是一个特例?

  • 这些列表实际上不再能给出程序流程的可靠概述:大多数后续计算只会作为某些输入操作的结果“公布”。所以我们不妨放弃列表结构,只需对每个输出操作进行“然后再做”。

data IO₂ = TxtOut String IO₂
         | TxtIn (String -> IO₂)
         | Terminate

main₄ :: IO₂
main₄ = TxtIn $ \_ ->
         TxtOut "Hello World"
          Terminate

还不错!

那么所有这些与monads有什么关系?

实际上,您不希望使用普通构造函数来定义所有程序。需要有一些这样的基本构造函数,但对于大多数更高级的东西,我们想要编写一个具有一些不错的高级签名的函数。事实证明,大多数这些看起来非常相似:接受某种有意义类型的值,并作为结果产生IO动作。

getTime :: (UTCTime -> IO₂) -> IO₂
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO₂
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO₂

这里有明显的模式,我们最好把它写成

type IO₃ a = (a -> IO₂) -> IO₂    -- If this reminds you of continuation-passing
                                  -- style, you're right.

getTime :: IO₃ UTCTime
randomRIO :: Random r => (r,r) -> IO₃ r
findFile :: RegEx -> IO₃ (Maybe FilePath)

现在开始看起来很熟悉了,但我们仍然只是处理欺骗性的简单普通功能,这是有风险的:每个“价值行动”都有责任实际传递任何包含的任何内容功能(否则整个程序的控制流很容易被中间的一个不良行为破坏)。我们最好明确要求这个要求。好吧,事实证明那些是 monad法则,虽然我不确定如果没有标准的绑定/连接操作符我们就能真正制定它们。

无论如何,我们现在已经达到了一个具有正确monad实例的IO公式:

data IO₄ a = TxtOut String (IO₄ a)
           | TxtIn (String -> IO₄ a)
           | TerminateWith a

txtOut :: String -> IO₄ ()
txtOut s = TxtOut s $ TerminateWith ()

txtIn :: IO₄ String
txtIn = TxtIn $ TerminateWith

instance Functor IO₄ where
  fmap f (TerminateWith a) = TerminateWith $ f a
  fmap f (TxtIn g) = TxtIn $ fmap f . g
  fmap f (TxtOut s c) = TxtOut s $ fmap f c

instance Applicative IO₄ where
  pure = TerminateWith
  (<*>) = ap

instance Monad IO₄ where
  TerminateWith x >>= f = f x
  TxtOut s c >>= f = TxtOut s $ c >>= f
  TxtIn g >>= f = TxtIn $ (>>=f) . g

显然,这不是IO的有效实现,但它原则上是可用的。

答案 4 :(得分:3)

Monads 只是解决一类重复出现问题的便捷框架。首先,monad必须是仿函数(即必须支持映射而不查看元素(或它们的类型)),它们还必须带来绑定(或链接)操作和从元素类型(return)创建monadic值的方法。最后,bindreturn必须满足两个方程(左右身份),也称为monad定律。 (或者,可以将monad定义为flattening operation而不是绑定。)

list monad 通常用于处理非确定性。绑定操作选择列表中的一个元素(直观地将它们全部放在 parallel worlds 中),让程序员用它们做一些计算,然后将所有世界的结果组合成单个列表(通过连接,或展平,嵌套列表)。以下是如何在Haskell的monadic框架中定义置换函数:

perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
            let shortened = take index l ++ drop (index + 1) l
            trailer <- perm shortened
            return (leader : trailer)

以下是 repl 会话的示例:

*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]

应该注意,列表monad绝不是副作用计算。作为monad的数学结构(即符合上述接口和定律)并不意味着副作用,尽管副作用现象通常很好地适合于monadic框架。

答案 5 :(得分:3)

Monads主要用于在链中组合功能。周期。

现在它们的构成方式在现有的monad中有所不同,从而导致不同的行为(例如,模拟状态monad中的可变状态)。

关于monad的混乱是如此普遍,即构成函数的机制,它们可用于许多事情,从而导致人们相信monad是关于状态,关于IO等等,当他们只是关于&#34;撰写功能&#34;。

现在,关于monad的一个有趣的事情是,合成的结果总是类型&#34; M a&#34;,也就是说,标记为&#34; M&#34;的信封内的值。 。这个特性恰好很难实现,例如,纯粹与不纯的代码之间的明确分离:将所有不纯的动作声明为类型&#34; IO a&#34;的函数。在定义IO monad时,不提供任何功能来取出&#34; a&#34;价值来自&#34; IO a&#34;。结果是没有任何函数可以是纯粹的,同时从&#34; IO a&#34;中取出一个值,因为在保持纯粹的同时无法获取这样的值(函数必须在& #34; IO&#34; monad使用这样的值)。 (注意:嗯,没有什么是完美的,所以&#34; IO straitjacket&#34;可以使用&#34; unsafePerformIO:IO a - &gt; a&#34;因此污染了应该是纯函数的东西,但是这应该非常谨慎地使用,当你真的知道没有引入任何带有副作用的不纯代码时。

答案 6 :(得分:2)

如果您有类型构造函数函数返回该类型族的值,则需要monad。最后,您希望将这些功能组合在一起。这些是回答为什么的三个关键要素。

让我详细说明一下。您拥有IntStringReal以及Int -> StringString -> Real类型的功能,依此类推。您可以轻松地组合这些功能,以Int -> Real结尾。生活很好。

然后,有一天,您需要创建一个新的系列类型。这可能是因为您需要考虑不返回任何值(Maybe),返回错误(Either),多个结果(List)等等的可能性。

请注意Maybe是一个类型构造函数。它需要一个类型,如Int,并返回一个新类型Maybe Int。首先要记住的是,没有类型构造函数,没有monad。

当然,您希望在代码中使用类型构造函数,很快就会以Int -> Maybe StringString -> Maybe Float等函数结束。现在,您无法轻松组合您的功能。生活不再好。

这是monad来救援的时候。它们允许您再次组合这种功能。您只需要为&gt; == 更改的构图。

答案 7 :(得分:2)

为什么我们需要单子?

实际上,您可以在没有 个单子的情况下得到-菲利普·瓦德勒(Philip Wadler)的How to Declare an Imperative

(* page 25 *)
val echoML    : unit -> unit
fun echoML () = let val c = getcML () in
                if c = #"\n" then
                  ()
                else
                  (putcML c; echoML ())
                end

其中:

(* pages 25-26 *)
fun putcML c  = TextIO.output1(TextIO.stdOut,c);
fun getcML () = valOf(TextIO.input1(TextIO.stdIn));

是的,好的-您可能正在尝试学习Haskell,这就是为什么您最终到这里来的原因。碰巧的是,像Haskell这样的非严格语言中的I / O难题使monad变得如此突出-这就是为什么我选择I / O作为运行示例的原因。

现在,您可以像这样在Haskell中编写echo

echoH :: IO ()
echoH =  do c <- getChar
            if c == '\n' then
              return ()
            else
              putChar c >> echoH

或者这个:

echoH' :: IO ()
echoH' =  getChar   >>= \c ->
          if c == '\n' then return () else
          putChar c >> echoH'

但是您不能写下:

errcho    :: () -> ()
errcho () =  let c = getc () in
             if c == '\n' then
               ()
             else
               putc c ; errcho ()

 -- fake primitives!
(;)  :: a -> b -> b
putc :: Char -> ()
getc :: ()   -> Char

这不是合法的Haskell ...但是这个几乎是:

echo   :: OI -> ()
echo u =  let !u1:u2:u3:_ = parts u in
          let !c          = getchar u1 in
          if c == '\n' then () else putchar c u2 `seq` echo u3

其中:

data OI             -- abstract
parts :: OI -> [OI] -- primitive

 -- I'll leave these definitions to you ;-)
putchar :: Char -> OI -> ()
getchar :: OI -> Char
  • Bang-patterns是Haskell 2010的扩展;

  • Prelude.seq isn't actually sequential-您将需要seq的替代定义,例如:

       -- for GHC 8.6.5
      {-# LANGUAGE CPP #-}
      #define during seq
      import qualified Prelude(during)
    
      {-# NOINLINE seq #-}
      infixr  0 `seq`
      seq     :: a -> b -> b
      seq x y = Prelude.during x (case x of _ -> y)
    

    或:

       -- for GHC 8.6.5
      {-# LANGUAGE CPP #-}
      #define during seq
      import qualified Prelude(during)
      import GHC.Base(lazy)
    
      infixr 0 `seq`
      seq     :: a -> b -> b
      seq x y = Prelude.during x (lazy y)
    

    (是的-使用了更多扩展名,但每个定义都保留了这些扩展名。)

比较笨重,但这是常规的Haskell:

echo   :: OI -> ()
echo u =  case parts u of
            u1:u2:u3:_ -> case getchar u1 of
                            c -> if c == '\n' then () else
                                 case putchar c u2 of () -> echo u3

是的,这有点不可思议,但是与suitable definition of seqpartsthose curious OI值一起使用可以使您做的很整洁像这样:

runDialogue :: Dialogue -> OI -> ()    
runDialogue d =
    \u -> foldr seq () (yet (\l -> zipWith respond (d l) (parts u)))

respond :: Request -> OI -> Response
respond Getq     = getchar `bind` (unit . Getp)
respond (Putq c) = putchar c `bind` \_ -> unit Putp

其中:

 -- types from page 14
type Dialogue = [Response] -> [Request]

data Request  = Getq | Putq Char
data Response = Getp Char | Putp

yet      :: (a -> a) -> a
yet f    =  f (yet f)

unit     :: a -> (OI -> a)
unit x   =  \u -> part u `seq` x

bind     :: (OI -> a) -> (a -> (OI -> b)) -> (OI -> b)
bind m k =  \u -> case part u of (u1, u2) -> (\x -> x `seq` k x u2) (m u1)

part     :: OI -> (OI, OI)
part u   =  case parts u of u1:u2:_ -> (u1, u2)

它不起作用?试试看:

yet      :: (a -> a) -> a
yet f    =  y where y = f y

是的,连续输入OI ->会很烦人,如果这种I / O方法行得通,那么它就必须在任何地方都行得通。最简单的解决方案是:

type IO a = OI -> a

避免使用构造函数进行包装和展开的麻烦。类型更改还为main提供了备用的类型签名:

main :: OI -> ()

总结-尽管monad可能非常有用:

echo' :: OI -> ()
echo' =  getchar   `bind` \c ->
         if c == '\n' then unit () else
         putchar c `bind` \_ -> echo'

在Haskell中并不需要它们。