简体   繁体   English

`IO` 的 >>= 到底是如何工作的?

[英]How exactly does `IO`'s >>= work under the hood?

When explaining a concept like Monad s to a beginner, I think it is helpful to avoid any complicated Haskell terminology, or anything category theory-like.在向初学者解释Monad的概念时,我认为避免使用任何复杂的 Haskell 术语或任何类似类别理论的东西是有帮助的。 I think a nice way to explain it is to build up a motivation for the function a -> mb with a straightforward type like Maybe :我认为解释它的一个很好的方法是为函数a -> mb建立一个动机,并使用像Maybe这样的简单类型:

data Maybe = Just a | Nothing

It's all or nothing.要么全有要么全无。 But what if we have some functions f :: a -> Maybe b and g :: b -> Maybe c and we want a nice way to combine them?但是如果我们有一些函数f :: a -> Maybe bg :: b -> Maybe c并且我们想要一种很好的方式来组合它们呢?

andThen :: Maybe a -> (a -> Maybe b) -> Maybe b
andThen Nothing _ = Nothing
andThen (Just a) f = f a

comp :: Maybe Text
comp = f a `andThen` g
  where f g a = etc...

You can then move into saying andThen could be defined for a variety of types (eventually forming the monad typeclass)... a compelling next example to me would be IO .然后你可以开始说andThen可以定义为各种类型(最终形成 monad 类型类)......对我来说一个引人注目的下一个例子是IO But how would you define andThen for IO yourself?但是你会如何为IO定义andThen呢? This has lead me to a question of my own... my naive implementation of andThenIO would be like so:这让我想到了一个我自己的问题……我对andThenIO幼稚实现是这样的:

andThenIO :: IO a -> (a -> IO b) -> IO b
andThenIO io f = f (unsafePerformIO io) 

But I know this isn't what is actually going on when you >>= using IO .但是我知道当您>>=使用IO时,这并不是实际发生的事情。 Looking at the implementation of bindIO in GHC.Base I see this:查看bindIOGHC.Base的实现,我看到了这一点:

bindIO :: IO a -> (a -> IO b) -> IO b
bindIO (IO m) k = IO (\ s -> case m s of (# new_s, a #) -> unIO (k a) new_s)

And for unIO this:对于unIO

unIO :: IO a -> (State# RealWorld -> (# State# RealWorld, a #))
unIO (IO a) = a

Which seems to relate to the ST monad somehow, though my knowledge of ST is next to nothing... I suppose my question is, what exactly is the difference between my naive implementation, and an implementation that uses ST ?这似乎与ST monad 以某种方式有关,尽管我对ST了解几乎为零......我想我的问题是,我的幼稚实现和使用ST的实现之间到底有什么区别? Is my naive implementation useful given the example or not given it isn't actually going on under the hood (and could be a misleading explanation)考虑到示例,我的幼稚实现是否有用,或者没有考虑到它实际上并没有在幕后进行(并且可能是误导性的解释)

(Note: this answers to the "how to explain how IO works to a beginner" part. It does NOT attempt to explain the RealWorld# hack GHC uses. Indeed, the latter is not a good way to introduce IO .) (注意:这回答了“如何向初学者解释IO是如何工作的”部分。它并不试图解释RealWorld# hack GHC 使用。实际上,后者不是介绍IO的好方法。)

There are many ways to explain the IO monad to beginners.有很多方法可以向初学者解释 IO monad。 It is hard because different people mentally associate monads to different ideas.这很难,因为不同的人在心理上将 monad 与不同的想法联系起来。 You can use category theory, or describe them as programmable semicolons, or even as burritos .您可以使用类别理论,或者将它们描述为可编程分号,甚至是burritos

Because of this, when I tried to do that in the past, I generally tried many approaches until one of them "clicks" into the mental pattern of the learner.因此,当我过去尝试这样做时,我通常会尝试多种方法,直到其中一种方法“点击”到学习者的思维模式中。 Knowing their background helps a lot.了解他们的背景很有帮助。

Imperative closures强制关闭

For instance, when the learner is already familiar with some imperative language with closures, eg JavaScript, I tend to tell them they can pretend that the whole point of a Haskell program is to generate a JavaScript closure, which is then run using a JavaScript implementation.例如,当学习者已经熟悉一些带有闭包的命令式语言时,例如 JavaScript,我倾向于告诉他们他们可以假装 Haskell 程序的重点是生成一个 JavaScript 闭包,然后使用 JavaScript 实现运行它. In this make-believe explanation, a IO T type stands for an opaque type encapsulating a JavaScript closure, which, when run, will produce a value of type T , possibly after causing some side effects -- as JavaScript can do.在这个虚构的解释中, IO T类型代表封装了 JavaScript 闭包的不透明类型,当运行时,它可能会产生T类型的值,这可能会导致一些副作用——正如 JavaScript 可以做到的那样。

So, a value f :: IO String could be implemented as因此,值f :: IO String可以实现为

let f = () => {
    print("side effect");
    return "result";
    };

and g :: IO () could be implemented asg :: IO ()可以实现为

let g = () => {
    print("g here");
    return {};
    };

Now, assuming to have such f closure, how to invoke it from Haskell?现在,假设有这样的f闭包,如何从 Haskell 调用它? Well, one can not directly do that, since Haskell wants to keep side effects under control.嗯,不能直接这样做,因为 Haskell 想要控制副作用。 That is, we can not do f ++ "hi" or f() ++ "hi" .也就是说,我们不能做f ++ "hi"f() ++ "hi"

Instead, to "invoke a closure" we can bind it to main相反,要“调用闭包”,我们可以将其绑定到main

main :: IO ()
main = g

Indeed, main is the JavaScript closure which is generated by the whole Haskell program, and this will be invoked by the Haskell implementation.实际上, main是由整个 Haskell 程序生成的 JavaScript 闭包,它将被 Haskell 实现调用。

OK, now the question becomes: "how to invoke more than one closure?".好的,现在问题变成了:“如何调用多个闭包?”。 For that, one can introduce >> and pretend that it is implemented as为此,可以引入>>并假装它是作为

function andThenSimple(f, g) {
   return () => {
      f();
      return g();
      };
}

or, for >>= :或者,对于>>=

function andThen(f, g) {
   return () => {
      let x = f();
      return g(x)();  // pass x, and then invoke the resulting closure
      };
}

return is easier return更容易

function ret(x) {
   return () => x;
}

These function take a while to explain, but it is not that hard to grasp them if one understands closures.这些函数需要花一些时间来解释,但如果理解闭包,理解它们并不难。

Pure functional (AKA stay free)纯功能(又名免费入住)

Another option is to keep everything pure.另一种选择是保持一切纯洁。 Or at least as pure as possible.或者至少尽可能纯净。 One can pretend that IO a is an opaque type defined as可以假设IO a是一种定义为的不透明类型

data IO a
   = Return a
   | Output String (IO a)
   | Input (String -> IO a)
   -- ... other IO operations here

and then pretend that the value main :: IO () is then "run" by some imperative engine later on.然后假设值main :: IO ()稍后由某个命令式引擎“运行”。 A program like一个程序像

foo :: IO Int
foo = do
  l <- getLine
  putStrLn l
  putStrLn l
  return (length l)

actually means, according to this interpretation,实际上意味着,根据这种解释,

foo :: IO Int
foo = Input (\l -> Output l (Output l (Return (length l))))

Of course here return = Return , and defining >>= is a nice exercise.当然这里return = Return ,定义>>=是一个很好的练习。

Currying impurity咖喱杂质

Forget IO, monads, and all that stuff.忘记 IO、monads 和所有这些东西。 One could understand better two simple concepts人们可以更好地理解两个简单的概念

a -> b   -- pure function type
a ~> b   -- impure function type

the latter being a make-believe Haskell type.后者是一种虚构的 Haskell 类型。 Most programmers should be able to have a strong intuition about what these types represent.大多数程序员应该能够对这些类型代表什么有很强的直觉。

Now, in functional programming, we have currying, which is an isomorphism between现在,在函数式编程中,我们有柯里化,这是两者之间的同构

(a, b) -> c

and

a -> (b -> c)

After some thinking, one can see that impure functions should admit some currying as well.经过一番思考,你会发现不纯函数也应该允许一些柯里化。 One can indeed be convinced that there should be some isomorphism similar to确实可以确信应该存在一些类似于

(a, b) ~> c
   <===>
a ~> (b ~> c)

With some more thought, one can even understand that the first ~> in a ~> (b ~> c) is actually inaccurate.对于一些更多的思考,人们甚至可以理解的是,第一~>a ~> (b ~> c)实际上是不准确的。 The curried function above does not really perform side effects when a alone is being passed -- it is the passing of b which triggers the execution of the original uncurried function, causing side effects.a单独被传递时,上面的柯里化函数并没有真正执行副作用——正是b的传递触发了原始未柯里化函数的执行,从而导致了副作用。

So, with this in mind, we can think of the impure currying as因此,考虑到这一点,我们可以将不纯的柯里化视为

(a, b) ~> c
   <===>
a -> (b ~> c)
--^^-- pure!

As a particular case, we get the isomorphism作为一个特例,我们得到同构

(a, ()) ~> c
   <===>
a -> (() ~> c)

Further, since (a, ()) is isomorphic to a (some more convincing is required here), we can interpret currying as此外,由于(a, ())a同构(这里需要一些更有说服力的内容),我们可以将柯里化解释为

a ~> c
  <===>
a -> (() ~> c)

Now, if we baptize () ~> c as IO c , we get现在,如果我们 baptize () ~> c作为IO c ,我们得到

a ~> c
  <===>
a -> IO c

Ah-ha!啊哈! This tells us that we do not really need the general impure function type a ~> c .这告诉我们,我们真的不需要一般的不纯函数类型a ~> c As long as we have its special case IO c = () ~> c , we can represent (up to isomorphism) any a ~> c function.只要我们有它的特殊情况IO c = () ~> c ,我们就可以表示(直到同构)任何a ~> c函数。

From here, one can start to draw a mental picture about how IO c should work, and eventually realize its monadic structure.从这里开始,人们可以开始描绘IO c应该如何工作的心理图景,并最终实现其一元结构。 Essentially, this interpretation of IO c is now very similar to the one exploiting closures given above.从本质上讲,这种对IO c解释现在与上面给出的利用闭包的解释非常相似。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM