我查看了https://www.fpcomplete.com/blog/2017/06/tale-of-two-brackets,虽然略读了一些部分,但我仍然不太了解核心问题“StateT
很糟糕,IO
是好的”,除了模糊地感觉到 Haskell 允许人们编写糟糕的StateT
monad(或者在文章的最终示例中,我认为MonadBaseControl
不是)。StateT
在黑线鳕中,必须满足以下定律:
askUnliftIO >>= (\u -> liftIO (unliftIO u m)) = m
所以这似乎是说m
在使用askUnliftIO
. 但在我看来,在 中IO
,整个世界都可以是状态。例如,我可能正在读取和写入磁盘上的文本文件。
引用迈克尔的另一篇文章,
虚假纯度 我们说 WriterT 和 StateT 是纯粹的,从技术上讲它们是纯粹的。但说实话:如果您有一个完全存在于 StateT 中的应用程序,那么您将无法从纯代码中获得您想要的受限突变的好处。不妨直言不讳,并接受您有一个可变变量。
这让我认为情况确实如此:对于 IO,我们是诚实的,对于StateT
,我们对可变性并不诚实……但这似乎是另一个问题,而不是上面的法律试图表明的;毕竟MonadUnliftIO
是假设IO
。我很难从概念上理解如何IO
比其他东西更具限制性。
更新 1
睡觉后(一些),我仍然很困惑,但随着时间的推移,我逐渐变得越来越少。我为IO
. 我意识到id
README 中的存在。尤其是,
instance MonadUnliftIO IO where
askUnliftIO = return (UnliftIO id)
所以askUnliftIO
似乎会返回IO (IO a)
一个UnliftIO m
.
Prelude> fooIO = print 5
Prelude> :t fooIO
fooIO :: IO ()
Prelude> let barIO :: IO(IO ()); barIO = return fooIO
Prelude> :t barIO
barIO :: IO (IO ())
m
回到法律上,当在转换后的 monad ( ) 上进行往返时,状态似乎在 monad 中没有发生突变askUnliftIO
,其中往返是unLiftIO
-> liftIO
。
继续上面的例子barIO :: IO ()
,所以如果我们这样做barIO >>= (u -> liftIO (unliftIO u m))
,那么u :: IO ()
和unliftIO u == IO ()
,那么liftIO (IO ()) == IO ()
。**因此,由于一切基本上都是幕后的应用程序id
,我们可以看到没有更改任何状态,即使我们正在使用IO
. 至关重要的是,我认为,重要的是 in 的值a
永远不会运行,也不会因为使用askUnliftIO
. 如果确实如此,那么就像在 的情况下一样,如果randomIO :: IO a
我们不运行它,我们将无法获得相同的值askUnliftIO
。(下面的验证尝试1)
但是,看起来我们仍然可以对其他 Monad 做同样的事情,即使它们确实保持状态。但我也看到,对于某些 monad,我们可能无法做到这一点。考虑一个人为的例子:每次我们访问a
包含在有状态 monad 中的类型的值时,都会改变一些内部状态。
验证尝试 1
> fooIO >> askUnliftIO
5
> fooIOunlift = fooIO >> askUnliftIO
> :t fooIOunlift
fooIOunlift :: IO (UnliftIO IO)
> fooIOunlift
5
到目前为止很好,但对为什么会发生以下情况感到困惑:
> fooIOunlift >>= (\u -> unliftIO u)
<interactive>:50:24: error:
* Couldn't match expected type `IO b'
with actual type `IO a0 -> IO a0'
* Probable cause: `unliftIO' is applied to too few arguments
In the expression: unliftIO u
In the second argument of `(>>=)', namely `(\ u -> unliftIO u)'
In the expression: fooIOunlift >>= (\ u -> unliftIO u)
* Relevant bindings include
it :: IO b (bound at <interactive>:50:1)