为什么我们需要monad?

本文翻译自:Why do we need monads?

In my humble opinion the answers to the famous question "What is a monad?" 我谦虚地看到着名问题“什么是单子?”的答案。 , especially the most voted ones, try to explain what is a monad without clearly explaining why monads are really necessary . ,特别是投票最多的人,试着解释什么是monad而没有明确解释为什么monad真的是必要的 Can they be explained as the solution to a problem? 他们可以解释为问题的解决方案吗?


#1楼

参考:https://stackoom.com/question/1u4J1/为什么我们需要monad


#2楼

Why do we need monads? 为什么我们需要monad?

  1. We want to program only using functions . 我们只想使用函数进行编程。 ("functional programming (FP)" after all). (毕竟“功能编程(FP)”)。
  2. Then, we have a first big problem. 然后,我们遇到了第一个大问题。 This is a program: 这是一个程序:

    f(x) = 2 * x

    g(x,y) = x / y

    How can we say what is to be executed first ? 我们怎么说首先要执行的是什么 How can we form an ordered sequence of functions (ie a program ) using no more than functions ? 我们如何使用不多于函数形成有序的函数序列(即程序 )?

    Solution: compose functions . 解决方案: 撰写功能 If you want first g and then f , just write f(g(x,y)) . 如果你想要先g然后f ,只需写f(g(x,y)) This way, "the program" is a function as well: main = f(g(x,y)) . 这样,“程序”也是一个函数: main = f(g(x,y)) OK, but ... 好的但是 ...

  3. More problems: some functions might fail (ie g(2,0) , divide by 0). 更多问题:某些功能可能会失败 (即g(2,0) ,除以0)。 We have no "exceptions" in FP (an exception is not a function). FP中没有“异常” (异常不是函数)。 How do we solve it? 我们如何解决它?

    Solution: Let's allow functions to return two kind of things : instead of having g : Real,Real -> Real (function from two reals into a real), let's allow g : Real,Real -> Real | Nothing 解决方案:让我们允许函数返回两种东西 :而不是g : Real,Real -> Real (从两个实数到实数的函数),让我们允许g : Real,Real -> Real | Nothing g : Real,Real -> Real | Nothing (function from two reals into (real or nothing)). g : Real,Real -> Real | Nothing (从两个实数到(真实或无))。

  4. But functions should (to be simpler) return only one thing . 但是函数应该(更简单)只返回一件事

    Solution: let's create a new type of data to be returned, a " boxing type " that encloses maybe a real or be simply nothing. 解决方案:让我们创建一个要返回的新类型的数据,一个“ 拳击类型 ”,它可能包含一个真实的或者根本就没有。 Hence, we can have g : Real,Real -> Maybe Real . 因此,我们可以拥有g : Real,Real -> Maybe Real OK, but ... 好的但是 ...

  5. What happens now to f(g(x,y)) ? 现在f(g(x,y))什么? f is not ready to consume a Maybe Real . f还没准备好消耗一个Maybe Real And, we don't want to change every function we could connect with g to consume a Maybe Real . 而且,我们不想改变我们可以用g连接的每个函数来使用Maybe Real

    Solution: let's have a special function to "connect"/"compose"/"link" functions . 解决方案:让我们有一个特殊的功能来“连接”/“撰写”/“链接”功能 That way, we can, behind the scenes, adapt the output of one function to feed the following one. 这样,我们可以在幕后调整一个函数的输出来提供下一个函数。

    In our case: g >>= f (connect/compose g to f ). 在我们的例子中: g >>= f (连接/组成gf )。 We want >>= to get g 's output, inspect it and, in case it is Nothing just don't call f and return Nothing ; 我们想要>>=得到g的输出,检查它,如果它是Nothing只是不要调用f并返回Nothing ; or on the contrary, extract the boxed Real and feed f with it. 或者相反,提取盒装的Real并用它进给f (This algorithm is just the implementation of >>= for the Maybe type). (这个算法只是>>=对于Maybe类型的实现)。 Also note that >>= must be written only once per "boxing type" (different box, different adapting algorithm). 另请注意, >>=每个“拳击类型”(不同的框,不同的自适应算法)必须只写一次

  6. Many other problems arise which can be solved using this same pattern: 1. Use a "box" to codify/store different meanings/values, and have functions like g that return those "boxed values". 使用相同的模式可以解决许多其他问题:1。使用“框”来编纂/存储不同的含义/值,并使用像g这样的函数来返回那些“盒装值”。 2. Have a composer/linker g >>= f to help connecting g 's output to f 's input, so we don't have to change any f at all. 2.有一个作曲家/链接器g >>= f来帮助将g的输出连接到f的输入,所以我们根本不需要改变任何f

  7. Remarkable problems that can be solved using this technique are: 使用这种技术可以解决的显着问题是:

    • having a global state that every function in the sequence of functions ("the program") can share: solution StateMonad . 具有全局状态,函数序列中的每个函数(“程序”)都可以共享:解决方案StateMonad

    • We don't like "impure functions": functions that yield different output for same input. 我们不喜欢“不纯的函数”:为同一输入产生不同输出的函数。 Therefore, let's mark those functions, making them to return a tagged/boxed value: IO monad. 因此,让我们标记这些函数,使它们返回一个标记/盒装值: IO monad。

Total happiness! 总幸福!


#3楼

The answer is, of course, "We don't" . 答案当然是“我们没有” As with all abstractions, it isn't necessary. 与所有抽象一样,没有必要。

Haskell does not need a monad abstraction. Haskell不需要monad抽象。 It isn't necessary for performing IO in a pure language. 没有必要以纯语言执行IO。 The IO type takes care of that just fine by itself. IO类型本身就可以解决这个问题。 The existing monadic desugaring of do blocks could be replaced with desugaring to bindIO , returnIO , and failIO as defined in the GHC.Base module. 根据GHC.Base模块中的定义,可以使用desugaring to bindIOreturnIOfailIO替换现有的monadic desugaring do块。 (It's not a documented module on hackage, so I'll have to point at its source for documentation.) So no, there's no need for the monad abstraction. (这不是关于hackage的文档模块,所以我必须指出它的文档来源 。)所以不,不需要monad抽象。

So if it's not needed, why does it exist? 所以,如果不需要,它为什么存在? Because it was found that many patterns of computation form monadic structures. 因为发现许多计算模式形成了一元结构。 Abstraction of a structure allows for writing code that works across all instances of that structure. 抽象结构允许编写适用于该结构的所有实例的代码。 To put it more concisely - code reuse. 更简洁地说 - 代码重用。

In functional languages, the most powerful tool found for code reuse has been composition of functions. 在函数式语言中,代码重用最强大的工具是函数的组合。 The good old (.) :: (b -> c) -> (a -> b) -> (a -> c) operator is exceedingly powerful. 好的旧(.) :: (b -> c) -> (a -> b) -> (a -> c)运算符非常强大。 It makes it easy to write tiny functions and glue them together with minimal syntactic or semantic overhead. 它可以轻松编写微小的函数并将它们粘合在一起,只需最少的语法或语义开销。

But there are cases when the types don't work out quite right. 但是有些情况下类型不能正常运行。 What do you do when you have foo :: (b -> Maybe c) and bar :: (a -> Maybe b) ? 当你有foo :: (b -> Maybe c)bar :: (a -> Maybe b)时你会怎么做? foo . bar foo . bar doesn't typecheck, because b and Maybe b aren't the same type. foo . bar没有类型检查,因为bMaybe b不是同一类型。

But... it's almost right. 但是......这几乎是正确的。 You just want a bit of leeway. 你只想要一点余地。 You want to be able to treat Maybe b as if it were basically b . 你希望能够将Maybe b视为基本上是b It's a poor idea to just flat-out treat them as the same type, though. 但是,将它们视为相同的类型是一个糟糕的主意。 That's more or less the same thing as null pointers, which Tony Hoare famously called the billion-dollar mistake . 这与零指针大致相同,Tony Hoare称其为十亿美元的错误 。 So if you can't treat them as the same type, maybe you can find a way to extend the composition mechanism (.) provides. 因此,如果您不能将它们视为相同类型,也许您可​​以找到扩展组合机制(.)提供的方法。

In that case, it's important to really examine the theory underlying (.) . 在这种情况下,真正检查基础理论(.)是很重要的。 Fortunately, someone has already done this for us. 幸运的是,有人已经为我们这样做了。 It turns out that the combination of (.) and id form a mathematical construct known as a category . 事实证明, (.)id的组合形成了一个被称为类别的数学结构。 But there are other ways to form categories. 但是还有其他方法可以形成类别。 A Kleisli category, for instance, allows the objects being composed to be augmented a bit. 例如,Kleisli类别允许组合的对象稍微增强。 A Kleisli category for Maybe would consist of (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c) and id :: a -> Maybe a . Maybe的Kleisli类别包括(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)id :: a -> Maybe a That is, the objects in the category augment the (->) with a Maybe , so (a -> b) becomes (a -> Maybe b) . 也就是说,类别中的对象使用Maybe扩充(->) ,因此(a -> b)变为(a -> Maybe b)

And suddenly, we've extended the power of composition to things that the traditional (.) operation doesn't work on. 突然之间,我们将组合的力量扩展到了传统(.)操作不起作用的东西。 This is a source of new abstraction power. 这是新抽象力量的来源。 Kleisli categories work with more types than just Maybe . Kleisli类别的工作类型不仅仅是Maybe They work with every type that can assemble a proper category, obeying the category laws. 它们适用于可以组装适当类别的每种类型,遵守类别法。

  1. Left identity: id . f 左侧身份: id . f id . f = f id . f = f
  2. Right identity: f . id 正确的身份: f . id f . id = f f . id = f
  3. Associativity: f . (g . h) 相关性: f . (g . h) f . (g . h) = (f . g) . h f . (g . h) = (f . g) . h (f . g) . h

As long as you can prove that your type obeys those three laws, you can turn it into a Kleisli category. 只要您能证明您的类型符合这三个法则,您就可以将其转换为Kleisli类别。 And what's the big deal about that? 那有什么大不了的? Well, it turns out that monads are exactly the same thing as Kleisli categories. 嗯,事实证明monad与Kleisli类别完全相同。 Monad 's return is the same as Kleisli id . Monadreturn与Kleisli id相同。 Monad 's (>>=) isn't identical to Kleisli (.) , but it turns out to be very easy to write each in terms of the other. Monad(>>=)与Kleisli (.)不完全相同,但事实证明,就另一个而言,每个人都很容易。 And the category laws are the same as the monad laws, when you translate them across the difference between (>>=) and (.) . 当你在(>>=)(.)之间的差异上翻译时,类别定律与monad定律相同。

So why go through all this bother? 那么为什么要经历这一切呢? Why have a Monad abstraction in the language? 为什么在语言中使用Monad抽象? As I alluded to above, it enables code reuse. 正如我在上面提到的,它使代码重用成为可能。 It even enables code reuse along two different dimensions. 它甚至可以沿两个不同的维度重用代码。

The first dimension of code reuse comes directly from the presence of the abstraction. 代码重用的第一个维度直接来自抽象的存在。 You can write code that works across all instances of the abstraction. 您可以编写适用于抽象所有实例的代码。 There's the entire monad-loops package consisting of loops that work with any instance of Monad . 整个monad-loops包由循环组成,可以与Monad任何实例一起使用。

The second dimension is indirect, but it follows from the existence of composition. 第二个维度是间接的,但它来自组合的存在。 When composition is easy, it's natural to write code in small, reusable chunks. 当组合很容易时,用小的可重用块编写代码是很自然的。 This is the same way having the (.) operator for functions encourages writing small, reusable functions. 这与函数的(.)运算符鼓励编写小的可重用函数的方式相同。

So why does the abstraction exist? 那为什么抽象存在呢? Because it's proven to be a tool that enables more composition in code, resulting in creating reusable code and encouraging the creation of more reusable code. 因为它被证明是一种工具,可以在代码中实现更多的组合,从而创建可重用的代码并鼓励创建更多可重用的代码。 Code reuse is one of the holy grails of programming. 代码重用是编程的圣杯之一。 The monad abstraction exists because it moves us a little bit towards that holy grail. monad抽象之所以存在,是因为它让我们向这个圣杯移动了一点点。


#4楼

Benjamin Pierce said in TAPL 本杰明皮尔斯在TAPL说

A type system can be regarded as calculating a kind of static approximation to the run-time behaviours of the terms in a program. 类型系统可以被视为计算程序中术语的运行时行为的一种静态近似。

That's why a language equipped with a powerful type system is strictly more expressive, than a poorly typed language. 这就是为什么配备强大类型系统的语言比表达不良的语言更具表现力。 You can think about monads in the same way. 你可以用同样的方式思考monad。

As @Carl and sigfpe point, you can equip a datatype with all operations you want without resorting to monads, typeclasses or whatever other abstract stuff. 作为@Carl和sigfpe指出,您可以为数据类型配备所需的所有操作,而无需使用monad,类型类或其他任何抽象的东西。 However monads allow you not only to write reusable code, but also to abstract away all redundant detailes. 但是monad不仅允许您编写可重用的代码,还可以抽象出所有冗余的详细信息。

As an example, let's say we want to filter a list. 举个例子,假设我们要过滤一个列表。 The simplest way is to use the filter function: filter (> 3) [1..10] , which equals [4,5,6,7,8,9,10] . 最简单的方法是使用filter函数: filter (> 3) [1..10] ,等于[4,5,6,7,8,9,10]

A slightly more complicated version of filter , that also passes an accumulator from left to right, is 一个稍微复杂的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]

To get all i , such that i <= 10, sum [1..i] > 4, sum [1..i] < 25 , we can write 为了获得所有i ,使得i <= 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]

which equals [3,4,5,6] . 等于[3,4,5,6]

Or we can redefine the nub function, that removes duplicate elements from a list, in terms of filterAccum : 或者我们可以重新定义nub函数,从filterAccum方面删除列表中的重复元素:

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

nub' [1,2,4,5,4,3,1,8,9,4] equals [1,2,4,5,3,8,9] . nub' [1,2,4,5,4,3,1,8,9,4]等于[1,2,4,5,3,8,9] A list is passed as an accumulator here. 列表在此处作为累加器传递。 The code works, because it's possible to leave the list monad, so the whole computation stays pure ( notElem doesn't use >>= actually, but it could). 代码有效,因为它可以保留列表monad,所以整个计算保持纯粹( notElem不使用>>=实际上,但它可以)。 However it's not possible to safely leave the IO monad (ie you cannot execute an IO action and return a pure value — the value always will be wrapped in the IO monad). 但是,无法安全地离开IO monad(即,您无法执行IO操作并返回纯值 - 该值始终将包含在IO monad中)。 Another example is mutable arrays: after you have leaved the ST monad, where a mutable array live, you cannot update the array in constant time anymore. 另一个例子是可变数组:在你离开ST monad之后,一个可变数组存在,你不能再在恒定时间内更新数组。 So we need a monadic filtering from the Control.Monad module: 所以我们需要从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 executes a monadic action for all elements from a list, yielding elements, for which the monadic action returns True . filterM对列表中的所有元素执行filterM操作,产生元素,monadic操作返回True

A filtering example with an array: 带数组的过滤示例:

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]

prints [1,2,4,5,3,8,9] as expected. 按预期打印[1,2,4,5,3,8,9]

And a version with the IO monad, which asks what elements to return: 还有一个带有IO monad的版本,它会询问要返回的元素:

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

Eg 例如

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

And as a final illustration, filterAccum can be defined in terms of filterM : 并作为最终的插图, filterAccum可以在以下方面定义filterM

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

with the StateT monad, that is used under the hood, being just an ordinary datatype. 使用StateT monad,它只是一种普通的数据类型。

This example illustrates, that monads not only allow you to abstract computational context and write clean reusable code (due to the composability of monads, as @Carl explains), but also to treat user-defined datatypes and built-in primitives uniformly. 这个例子说明,monad不仅允许你抽象计算上下文并编写干净的可重用代码(由于monad的可组合性,如@Carl所解释的),而且还可以统一处理用户定义的数据类型和内置基元。


#5楼

I don't think IO should be seen as a particularly outstanding monad, but it's certainly one of the more astounding ones for beginners, so I'll use it for my explanation. 我不认为IO应该被视为一个特别出色的monad,但它肯定是初学者中最令人震惊的一个,所以我会用它来解释。

Naïvely building an IO system for Haskell Naïvely为Haskell构建一个IO系统

The simplest conceivable IO system for a purely-functional language (and in fact the one Haskell started out with) is this: 对于纯功能语言(实际上是Haskell开始使用的),最简单的可想象的IO系统是这样的:

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

With lazyness, that simple signature is enough to actually build interactive terminal programs – very limited, though. 有了懒惰,这个简单的签名足以实际构建交互式终端程序 - 但非常有限。 Most frustrating is that we can only output text. 最令人沮丧的是我们只能输出文字。 What if we added some more exciting output possibilities? 如果我们添加一些更令人兴奋的输出可能性怎

data Output = TxtOutput String
            | Beep Frequency

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

cute, but of course a much more realistic “alterative output” would be writing to a file . 可爱,但当然更现实的“替代输出”将写入文件 But then you'd also want some way to read from files. 但是你也想要一些文件中读取的方法。 Any chance? 任何机会?

Well, when we take our main₁ program and simply pipe a file to the process (using operating system facilities), we have essentially implemented file-reading. 好吧,当我们使用main₁程序并简单地将文件传递给进程 (使用操作系统工具)时,我们基本上实现了文件读取。 If we could trigger that file-reading from within the Haskell language... 如果我们可以从Haskell语言中触发该文件读取...

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

This would use an “interactive program” String->[Output] , feed it a string obtained from a file, and yield a non-interactive program that simply executes the given one. 这将使用“交互式程序” String->[Output] ,将其从文件中获取一个字符串,并生成一个简单执行给定程序的非交互式程序。

There's one problem here: we don't really have a notion of when the file is read. 这里有一个问题:我们实际上没有关于何时读取文件的概念。 The [Output] list sure gives a nice order to the outputs , but we don't get an order for when the inputs will be done. [Output]列表肯定带来一个很好的为输出 ,但我们没有得到,当输入将完成的订单。

Solution: make input-events also items in the list of things to do. 解决方案:使输入事件也成为要执行的事项列表中的项目。

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"]
          ]

Ok, now you may spot an imbalance: you can read a file and make output dependent on it, but you can't use the file contents to decide to eg also read another file. 好吧,现在你可能发现了一个不平衡:你可以读取一个文件并使输出依赖它,但你不能使用文件内容来决定例如也读取另一个文件。 Obvious solution: make the result of the input-events also something of type IO , not just Output . 明显的解决方案:使输入事件的结果也是类型IO ,而不仅仅是Output That sure includes simple text output, but also allows reading additional files etc.. 这确实包括简单的文本输出,但也允许读取其他文件等。

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

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

That would now actually allow you to express any file operation you might want in a program (though perhaps not with good performance), but it's somewhat overcomplicated: 现在,这实际上允许您在程序中表达您可能想要的任何文件操作(尽管可能没有良好的性能),但它有点过于复杂:

  • main₃ yields a whole list of actions. main₃产生的操作的完整列表 Why don't we simply use the signature :: IO₁ , which has this as a special case? 为什么我们不使用签名:: IO₁ ,这是一个特例?

  • The lists don't really give a reliable overview of program flow anymore: most subsequent computations will only be “announced” as the result of some input operation. 这些列表实际上不再给出程序流程的可靠概述:大多数后续计算仅作为某些输入操作的结果被“公布”。 So we might as well ditch the list structure, and simply cons a “and then do” to each output operation. 所以我们不妨放弃列表结构,只需对每个输出操作进行“然后再做”。

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

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

Not too bad! 还不错!

So what has all of this to do with monads? 那么所有这些与monads有什么关系呢?

In practice, you wouldn't want to use plain constructors to define all your programs. 实际上,您不希望使用普通构造函数来定义所有程序。 There would need to be a good couple of such fundamental constructors, yet for most higher-level stuff we would like to write a function with some nice high-level signature. 需要有一些这样的基本构造函数,但对于大多数更高级的东西,我们想要编写一个具有一些不错的高级签名的函数。 It turns out most of these would look quite similar: accept some kind of meaningfully-typed value, and yield an IO action as the result. 事实证明,大多数这些看起来非常相似:接受某种有意义类型的值,并产生IO动作作为结果。

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

There's evidently a pattern here, and we'd better write it as 这里显然有一种模式,我们最好把它写成

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)

Now that starts to look familiar, but we're still only dealing with thinly-disguised plain functions under the hood, and that's risky: each “value-action” has the responsibility of actually passing on the resulting action of any contained function (else the control flow of the entire program is easily disrupted by one ill-behaved action in the middle). 现在开始看起来很熟悉了,但我们仍然只是处理欺骗性的简单功能,而且风险很大:每个“价值行动”都有责任实际传递任何包含函数的结果动作(否则整个程序的控制流很容易被中间的一个不良行为打乱。 We'd better make that requirement explicit. 我们最好明确要求这个要求。 Well, it turns out those are the monad laws , though I'm not sure we can really formulate them without the standard bind/join operators. 好吧,事实证明这些是monad定律 ,但我不确定如果没有标准的绑定/连接运算符,我们可以真正制定它们。

At any rate, we've now reached a formulation of IO that has a proper monad instance: 无论如何,我们现在已经达到了一个具有正确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

Obviously this is not an efficient implementation of IO, but it's in principle usable. 显然,这不是IO的有效实现,但它原则上是可用的。


#6楼

You need monads if you have a type constructor and functions that returns values of that type family . 如果您有类型构造函数返回该类型族值的函数,则需要monad。 Eventually, you would like to combine these kind of functions together . 最后,您希望将这些功能组合在一起 These are the three key elements to answer why . 这些是回答原因的三个关键要素。

Let me elaborate. 让我详细说明一下。 You have Int , String and Real and functions of type Int -> String , String -> Real and so on. 你有IntStringReal以及Int -> StringString -> Real等类型的函数。 You can combine these functions easily, ending with Int -> Real . 您可以轻松地组合这些功能,以Int -> Real结尾。 Life is good. 生活很好。

Then, one day, you need to create a new family of types . 然后,有一天,你需要创建一个类型新的家庭 It could be because you need to consider the possibility of returning no value ( Maybe ), returning an error ( Either ), multiple results ( List ) and so on. 这可能是因为您需要考虑不返回值( Maybe ),返回错误( Either ),多个结果( List )等的可能性。

Notice that Maybe is a type constructor. 请注意, Maybe是一个类型构造函数。 It takes a type, like Int and returns a new type Maybe Int . 它需要一个类型,如Int并返回一个新类型Maybe Int First thing to remember, no type constructor, no monad. 首先要记住的是, 没有类型构造函数,没有monad。

Of course, you want to use your type constructor in your code, and soon you end with functions like Int -> Maybe String and String -> Maybe Float . 当然, 您希望在代码中使用类型构造函数 ,很快就会以Int -> Maybe StringString -> Maybe Float等函数结束。 Now, you can't easily combine your functions. 现在,您无法轻松组合您的功能。 Life is not good anymore. 生活不再好。

And here's when monads come to the rescue. 而且这是monad来救援的时候。 They allow you to combine that kind of functions again. 它们允许您再次组合这种功能。 You just need to change the composition . 你只需要改变构图 for >== . for > ==

你可能感兴趣的:(haskell,monads)