这是我最后一个问题的后续。 嵌套在其他未执行的单元中的 IO 操作
该问题的解决方案是删除一些monads,这允许IO操作执行。
为什么我需要解开单子的巢穴?有没有办法在不取消嵌套的情况下执行 IO?
注意:这是一个假设,而不是一个关于好或坏做法的问题。
也许将IO
视为type IO a = World -> (a, World)
会有所帮助; 也就是说,一个函数将计算机的当前状态作为其唯一参数,并返回新状态以及一些值a
。这与 GHC 内部中IO
的实际实现没有太大区别,所以希望我们可以原谅这里通过类比进行通信的(卑鄙)方法。
所以readFile :: FilePath -> IO String
,例如,变得readFile :: FilePath -> World -> (a, World)
.
main :: IO ()
真的很main :: World -> ((), World)
.
但是,这意味着类型为IO _
的值是惰性的。它们只是功能!函数在被赋予值之前无法执行任何操作;在我们的例子中,函数想要的值是一个World
对象,我们无法构造它。这就是 Haskell 中 IO 的美妙之处:我们可以通过使用我们熟悉和喜爱的 monadic 运算符(return、bind)来构建一个IO
操作,但在运行时传入World
对象之前,它不能做任何事情。
这意味着我们构建的任何未通过线程main
IO
操作都不会被执行。
因此,使用foobar :: [Char] -> IO [IO ()]
,我们当然可以观察到返回值:
main :: IO ()
main = do
ios <- foobar "string"
print "goodbye"
但直到我们解构ios
并绑定内部IO
值,这些操作才能获得它们想要的World
:
main :: IO ()
main = do
ios <- foobar
ios !! 0
ios !! 1
ios !! 2
...
print "goodbye"
或者,简称,
main = do
ios <- foobar
sequence ios
print "goodbye"
希望这有帮助。
让我们从一个稍微不同的例子开始。如您所知,String
是Char
的列表:
GHCi> :set +t
GHCi> "Mississippi"
"Mississippi"
it :: [Char]
Strings
列表是Char
列表的列表;也就是说,[[Char]]
:
GHCi> group "Mississippi"
["M","i","ss","i","ss","i","pp","i"]
it :: [[Char]]
group "Mississippi"
是一个[[Char]]
,我不希望它作为一个[Char]
来处理——那会破坏使用group
的意义。
在大多数情况下,IO a
值与其他任何值一样,因此适用类似的注意事项。举一个具体(和现实)的例子,假设我们有一个这种类型的函数......
(KeyCode -> IO ()) -> IO (IO ())
。在 GUI 中注册按键事件的事件处理程序。这个想法是使用KeyCode -> IO ()
参数调用函数,该参数指定响应按键时应发生的情况,并运行生成的IO (IO ())
,以便您选择的KeyCode -> IO ()
处理程序变为活动状态。但是,IO (IO ())
操作生成的内部IO ()
具有不同的用途:它取消注册事件处理程序,并且由您自行决定在应用程序的稍后点使用(也许永远不会)。在这种情况下,您绝对不想在外部操作之后立即运行内部操作!
总而言之,IO (IO a)
是一个IO
操作,运行时会产生另一个IO
操作,您可能也希望也可能不想运行该操作。
PS:正如Sheyll在其他问答中提到的,join
可用于扁平化嵌套IO
操作或任何其他嵌套的monadic值。顺便说一下,列表也有一个Monad
实例。你认为join (group "Mississippi")
会怎么做?
嗯...你问这个问题:
为什么我需要解开单子的巢穴?有没有办法在不取消嵌套的情况下执行 IO?
好吧,让我直截了当地说:在哈斯克尔土地上,我们怎么称呼它,不做嵌套或join
,不仅仅是另一个单子组合器,它是一个神圣的特殊哼哼,Monad
区别于Functor
和Applicative
!
是的,这完全意味着Monad
类型类可以使用join
方法而不是>>=
进行设计。
实际上,解嵌套和绑定是同一事物的两种不同视角!
让我剧透这篇文章的其余部分:
join = (>>= id)
。和:
(ma >>= amb) = join (amb <$> ma)
让我们通过证明我们可以对>>=
进行join
来证明它们是平等的,反之亦然。
用>>=
制作join
好的,现在更详细地join = (>>= id)
:
join mmx = do mx <- mmx
x <- mx
return x
然后:
join mmx = do mx <- mmx
mx
现在有了bind
又名>>=
:
join mmx = mmx >>= id
和无积分使用部分:
join = (>>= id)
用join
制作>>=
现在反过来,这更难,我们需要这样一个事实,即每个Monad
也是一个Functor
。
记住>>=
实际上做了什么(双关语):
ma >>= amb = do a <- ma
amb a
我们知道amb
是a -> m b
类型的函数,这个想法是使用fmap
它有(c -> d) -> m c -> m d
,如果我们fmap
amb
我们得到一个表达式fmap amb
那么c
变得a
,d
变得m b
,因此m c -> m d
变得m a -> m (m b)
!
现在我们很激动:m (m b)
只是尖叫join
,我们只需要输入正常应用程序可以做的m a
:
ma >>= amb = do mb <- fmap amb ma
mb
这是我们在上一节中看到的:
join mmb = do mb <- mmb
mb
与mmb == fmap amb ma
ma >>= amb == do mb <- fmap amb ma == join (fmap amb ma)
mb
给你。