"IO`'s >>究竟是如何在引擎盖下工作的?

dan*_*oks 5 io monads haskell

在向Monad初学者解释像s 这样的概念时,我认为避免任何复杂的Haskell术语或任何类别理论都是有帮助的.我认为解释它的一个很好的方法是为这个函数建立一个动机,a -> m b如下所示Maybe:

data Maybe = Just a | Nothing
Run Code Online (Sandbox Code Playgroud)

这是全有或全无.但是,如果我们有一些功能f :: a -> Maybe b并且g :: 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...
Run Code Online (Sandbox Code Playgroud)

然后你可以进入说andThen可以为各种类型定义(最终形成monad类型类)......对我来说,一个引人注目的下一个例子就是IO.但是你会如何andThenIO自己定义?这引出了我自己的问题......我的天真实现andThenIO会是这样的:

andThenIO :: IO a -> (a -> IO b) -> IO b
andThenIO io f = f (unsafePerformIO io) 
Run Code Online (Sandbox Code Playgroud)

但我知道这不是你>>=使用时实际发生的事情IO.综观实行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)
Run Code Online (Sandbox Code Playgroud)

而对于unIO这一点:

unIO :: IO a -> (State# RealWorld -> (# State# RealWorld, a #))
unIO (IO a) = a
Run Code Online (Sandbox Code Playgroud)

这似乎与STmonad有某种关系,虽然我的知识几乎ST没有......我想我的问题是,我的天真实现与使用的实现之间究竟有什么区别ST?考虑到这个例子,我的天真实现是否有用,因为它实际上并没有在幕后进行(可能是误导性的解释)

chi*_*chi 10

(注:这回答了"如何解释如何IO工作的一个初学者的部分"它不试图解释.RealWorld#GHC使用的确黑客,后者不是引进一个好办法.IO)

有许多方法可以向初学者解释IO monad.这很难,因为不同的人在心理上将monad与不同的想法联系起来.您可以使用类别理论,或将它们描述为可编程分号,甚至可以将其描述为卷饼.

因此,当我过去尝试这样做时,我通常会尝试很多方法,直到其中一个方法"点击"进入学习者的心理模式.了解他们的背景有很大帮助.

势在必行的封闭

例如,当学习者已经熟悉一些带闭包的命令式语言时,例如JavaScript,我倾向于告诉他们他们可以假装Haskell程序的重点是生成一个JavaScript闭包,然后使用JavaScript实现运行.在这个虚构的解释,一个IO T类型代表不透明型封装的JavaScript关闭,其中,在运行时,会产生类型的值T,可能引起一些副作用之后-如JavaScript可以做到.

因此,一个值f :: IO String可以实现为

let f = () => {
    print("side effect");
    return "result";
    };
Run Code Online (Sandbox Code Playgroud)

g :: IO ()可以实现为

let g = () => {
    print("g here");
    return {};
    };
Run Code Online (Sandbox Code Playgroud)

现在,假设有这样的f闭包,如何从Haskell调用它?好吧,人们不能直接这样做,因为Haskell希望控制副作用.也就是说,我们不能做f ++ "hi"f() ++ "hi".

相反,为了"调用一个闭包",我们可以将它绑定到 main

main :: IO ()
main = g
Run Code Online (Sandbox Code Playgroud)

实际上,main是由整个Haskell程序生成的JavaScript闭包,这将由Haskell实现调用.

好的,现在问题变成:"如何调用多个闭包?".为此,可以引入>>并假装它被实现为

function andThenSimple(f, g) {
   return () => {
      f();
      return g();
      };
}
Run Code Online (Sandbox Code Playgroud)

或者,用于>>=:

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

return 更容易

function ret(x) {
   return () => x;
}
Run Code Online (Sandbox Code Playgroud)

这些函数需要一段时间才能解释,但如果理解闭包,就不难理解它们.

纯功能(AKA免费)

另一种选择是保持一切纯净.或者至少尽可能纯净.可以假设这IO a是一个定义为的不透明类型

data IO a
   = Return a
   | Output String (IO a)
   | Input (String -> IO a)
   -- ... other IO operations here
Run Code Online (Sandbox Code Playgroud)

然后假装main :: IO ()某个命令式引擎随后"运行" 该值.像这样的程序

foo :: IO Int
foo = do
  l <- getLine
  putStrLn l
  putStrLn l
  return (length l)
Run Code Online (Sandbox Code Playgroud)

实际上,根据这种解释,

foo :: IO Int
foo = Input (\l -> Output l (Output l (Return (length l))))
Run Code Online (Sandbox Code Playgroud)

当然在这里return = Return,定义>>=是一个很好的练习.

干燥杂质

忘记IO,monads和所有这些东西.人们可以理解更好的两个简单概念

a -> b   -- pure function type
a ~> b   -- impure function type
Run Code Online (Sandbox Code Playgroud)

后者是一种虚构的Haskell类型.大多数程序员应该能够对这些类型所代表的内容有很强的直觉.

现在,在函数式编程中,我们有currying,这是一个同构之间

(a, b) -> c
Run Code Online (Sandbox Code Playgroud)

a -> b -> c
Run Code Online (Sandbox Code Playgroud)

经过一番思考后,人们可以看到不纯的功能也应该承认一些曲解.人们确实可以确信应该有一些类似的同构

(a, b) ~> c
   <===>
a ~> b ~> c
Run Code Online (Sandbox Code Playgroud)

对于一些更多的思考,人们甚至可以理解的是,第一~>a ~> b ~> c其实是不准确的.当a单独传递时,上面的curried函数并不真正执行副作用- 它的传递b会触发原始未传递函数的执行,从而导致副作用.

因此,考虑到这一点,我们可以将currying视为

(a, b) ~> c
   <===>
a -> b ~> c
--^^-- pure!
Run Code Online (Sandbox Code Playgroud)

作为一个特例,我们得到了同构

(a, ()) ~> c
   <===>
a -> () ~> c
Run Code Online (Sandbox Code Playgroud)

此外,由于(a, ())是同形的a(这里需要更有说服力),我们可以将currying解释为

a ~> c
  <===>
a -> () ~> c
Run Code Online (Sandbox Code Playgroud)

现在,如果我们施洗() ~> cIO c,我们得到

a ~> c
  <===>
a -> IO c
Run Code Online (Sandbox Code Playgroud)

啊,哈!这告诉我们,我们并不真正需要一般的不纯函数类型a ~> c.只要我们有其特殊情况IO c = () ~> c,我们就可以表示(直到同构)任何a ~> c函数.

从这里开始,人们可以开始画出一幅关于IO c应该如何工作的心理图画,并最终实现它的一元结构.从本质上讲,这种解释IO c现在非常类似于上面给出的利用闭包的解释.

  • 我完全赞同将"IO"作为数据类型,但我认为使用存在量化(为了清晰起见,使用GADT语法)是一个非常重要的优化,即使对于初学者也是值得介绍的.这允许`:>> =`或`:= <<`构造函数,所以`>> =`需要恒定的时间.由于"IO"接口不允许检查,因此足以在恒定因子下提供良好的性能.`data IO :: Type - > Type其中{Pure :: a - > IO a; (:= <<)::(a - > IO b) - > IO a - > IO b; PutStr :: String - > IO(); GetLine :: IO String; ...}` (2认同)