haskell - 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. I think a nice way to explain it is to build up a motivation for the function a -> m b
with a straightforward type like 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?
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
. But how would you define andThen
for IO
yourself? This has lead me to a question of my own... my naive implementation of andThenIO
would be like so:
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
. Looking at the implementation of bindIO
in GHC.Base
I see this:
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 :: 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
? 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)
解决方案
(注意:这回答了“如何向IO
初学者解释如何工作”部分。它并不试图解释RealWorld#
GHC 使用的 hack。事实上,后者不是介绍的好方法IO
。)
有很多方法可以向初学者解释 IO monad。这很难,因为不同的人在心理上将 monad 与不同的想法联系起来。您可以使用类别理论,或者将它们描述为可编程分号,甚至是墨西哥卷饼。
正因为如此,当我过去尝试这样做时,我通常会尝试很多方法,直到其中一种方法“点击”到学习者的心理模式中。了解他们的背景很有帮助。
命令式闭包
例如,当学习者已经熟悉一些带有闭包的命令式语言(例如 JavaScript)时,我倾向于告诉他们,他们可以假装 Haskell 程序的全部目的是生成一个 JavaScript 闭包,然后使用 JavaScript 实现来运行该闭包. 在这个虚构的解释中,IO T
类型代表封装 JavaScript 闭包的不透明类型,它在运行时会产生一个 type 的值T
,可能在引起一些副作用之后——就像 JavaScript 可以做的那样。
因此,一个值f :: IO String
可以实现为
let f = () => {
print("side effect");
return "result";
};
并且g :: IO ()
可以实现为
let g = () => {
print("g here");
return {};
};
现在,假设有这样的f
闭包,如何从 Haskell 调用它?好吧,不能直接这样做,因为 Haskell 想要控制副作用。也就是说,我们不能做f ++ "hi"
or f() ++ "hi"
。
相反,要“调用闭包”,我们可以将其绑定到main
main :: IO ()
main = g
实际上,main
是由整个 Haskell 程序生成的 JavaScript 闭包,它将由 Haskell 实现调用。
好的,现在问题变成了:“如何调用多个闭包?”。为此,可以引入>>
并假装它被实现为
function andThenSimple(f, g) {
return () => {
f();
return g();
};
}
或者,对于>>=
:
function andThen(f, g) {
return () => {
let x = f();
return g(x)(); // pass x, and then invoke the resulting closure
};
}
return
更容易
function ret(x) {
return () => x;
}
这些功能需要一段时间来解释,但如果理解闭包,掌握它们并不难。
纯功能(AKA 保持免费)
另一种选择是保持一切纯净。或者至少尽可能纯净。可以假装它IO a
是一种不透明类型,定义为
data IO a
= Return a
| Output String (IO a)
| Input (String -> IO a)
-- ... other IO operations here
然后假装该值main :: IO ()
稍后由某个命令式引擎“运行”。像这样的程序
foo :: IO Int
foo = do
l <- getLine
putStrLn l
putStrLn l
return (length l)
实际上,根据这种解释,
foo :: IO Int
foo = Input (\l -> Output l (Output l (Return (length l))))
当然在这里return = Return
,定义>>=
是一个很好的练习。
咖喱杂质
忘记 IO、monad 和所有这些东西。一个人可以更好地理解两个简单的概念
a -> b -- pure function type
a ~> b -- impure function type
后者是一种虚构的 Haskell 类型。大多数程序员应该能够对这些类型代表什么有强烈的直觉。
现在,在函数式编程中,我们有柯里化,它是
(a, b) -> c
和
a -> (b -> c)
经过一番思考,可以看出不纯函数也应该承认一些柯里化。确实可以确信应该有一些同构类似于
(a, b) ~> c
<===>
a ~> (b ~> c)
再想一想,甚至可以理解第一个~>
实际上a ~> (b ~> c)
是不准确的。上面的柯里化函数在单独传递时并没有真正产生副作用a
——它的传递b
触发了原始未柯里化函数的执行,从而产生了副作用。
因此,考虑到这一点,我们可以将不纯的柯里化视为
(a, b) ~> c
<===>
a -> (b ~> c)
--^^-- pure!
作为一个特例,我们得到同构
(a, ()) ~> c
<===>
a -> (() ~> c)
此外,由于(a, ())
是同构的a
(这里需要一些更有说服力的),我们可以将柯里化解释为
a ~> c
<===>
a -> (() ~> c)
现在,如果我们() ~> c
为施洗IO c
,我们得到
a ~> c
<===>
a -> IO c
啊哈!这告诉我们,我们并不真正需要一般的不纯函数类型a ~> c
。只要我们有它的特殊情况IO c = () ~> c
,我们就可以表示(直到同构)任何a ~> c
函数。
从这里开始,人们可以开始在脑海中描绘IO c
应该如何工作,并最终实现它的一元结构。本质上,这种解释IO c
现在与上面给出的利用闭包的解释非常相似。
推荐阅读
- javascript - 为什么出现 input_Login 在浏览器中未定义?
- debugging - VSCode 中的绿色调试器指针表示什么?
- asp.net-core - IdentityServer4,WindowsCryptographicException:系统找不到指定的文件
- pytorch - 找不到有效的 cuDNN 算法来运行卷积
- haskell - 如何将 Control.Monad.Reader 中的 mapReader 用于 reader monad?
- c++ - 打破 2 组中 1 到 n 个数的排列
- r - 在 Ubuntu 上安装 semMediation 包的问题
- ruby - 在循环的每次迭代中写入 Ruby 中的文件
- javascript - Chai-http/mocha 总是返回一个 HTTP 404
- mongodb - 对 MongoDB 的初始 POST 在我没有添加任何内容的数组中创建一个空对象