gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-02 14:40:48 25 4
gpt4 key购买 nike

当向初学者解释诸如Monad之类的概念时,我认为避免使用任何复杂的Haskell术语或类似类别理论的任何内容都是有帮助的。我认为,解释它的一种好方法是使用诸如a -> m b这样的简单类型为Maybe函数建立动力:

data Maybe = Just a | Nothing

全部或全无。但是,如果我们有一些函数 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...

然后,您可以说可以为各种类型定义 andThen(最终形成monad typeclass)...对我来说,下一个令人信服的示例是 IO。但是,您如何自己为 andThen定义 IO?这使我想到了一个自己的问题...我对 andThenIO的幼稚实现如下:
andThenIO :: IO a -> (a -> IO b) -> IO b
andThenIO io f = f (unsafePerformIO io)

但是我知道这不是当您使用 >>=进行 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)

而对于 unIO来说:
unIO :: IO a -> (State# RealWorld -> (# State# RealWorld, a #))
unIO (IO a) = a

尽管我对 ST的了解几乎为零,但这似乎与 ST monad有关……我想我的问题是,我的幼稚实现和使用 ST的实现之间到底有什么区别?在给出示例的情况下,我幼稚的实现是否有用?或者给出的示例实际上并未在幕后进行(这可能是一个误导性的解释)

最佳答案

(注意:这回答了“如何向初学者解释IO的工作原理”部分。它没有尝试解释GHC使用的RealWorld# hack。实际上,后者不是介绍IO的好方法。)
有很多方法可以向初学者解释IO monad。很难,因为不同的人在精神上将单子(monad)与不同的思想联系在一起。您可以使用类别理论,或将它们描述为可编程分号,甚至描述为burritos
因此,过去我尝试这样做时,通常会尝试许多方法,直到其中一种“点击”学习者的思维模式。了解他们的背景会很有帮助。
强制闭包
举例来说,当学习者已经熟悉某些命令式语言时,例如:我倾向于告诉JavaScript他们可以假装Haskell程序的全部要点是生成JavaScript闭包,然后使用JavaScript实现运行该闭包。在这种相信的解释中,IO T类型代表封装JavaScript闭包的不透明类型,当运行时,它可能会产生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"f() ++ "hi"
相反,要“调用闭包”,我们可以将其绑定(bind)到 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;
}
这些功能需要花一些时间来解释,但是如果您了解闭包,就不难掌握它们。
纯功能性(又名免费)
另一种选择是保持一切纯净。或至少尽可能纯。可以假装 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类型。大多数程序员应该对这些类型代表什么有很强的直觉。
现在,在函数式编程中,我们有currying,这是
(a, b) -> c
a -> (b -> c)
经过一番思考,人们可以看到不纯函数也应该承认一些麻烦。确实可以确信,应该有一些同构,类似于
(a, b) ~> c
<===>
a ~> (b ~> c)
再三考虑,甚至可以理解 ~>中的第一个 a ~> (b ~> c)实际上是不准确的。当仅传递 a时,上面的curried函数并不会真正产生副作用-传递 b会触发原始非curried函数的执行,从而产生副作用。
因此,考虑到这一点,我们可以认为不纯的curring为
(a, b) ~> c
<===>
a -> (b ~> c)
--^^-- pure!
作为特殊情况,我们得到同构
(a, ()) ~> c
<===>
a -> (() ~> c)
此外,由于 (a, ())a同构(此处需要更多说服力),因此我们可以将curring解释为
a ~> c
<===>
a -> (() ~> c)
现在,如果我们将 () ~> c洗礼为 IO c,我们得到
a ~> c
<===>
a -> IO c
啊哈!这告诉我们,我们实际上不需要通用的不纯函数类型 a ~> c。只要我们有其特殊情况 IO c = () ~> c,我们就可以表示(直到同构)任何 a ~> c函数。
从这里开始,人们可以开始思考 IO c应该如何工作,并最终实现其单子(monad)结构。从本质上讲,现在对 IO c的解释与上面给出的利用闭包的解释非常相似。

关于haskell - `IO`的>> =到底如何工作?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51770808/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com