如何理解 `MonadUnliftIO` 对 "no stateful monads" 的要求?

How to understand `MonadUnliftIO`'s requirement of "no stateful monads"?

我看了 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

所以这似乎是说当使用 askUnliftIO 时,monad m 中的状态没有发生变化。但在我看来,在 IO 中,整个世界都可以是状态。例如,我可以读取和写入磁盘上的文本文件。

引用another article by Michael,

False purity We say WriterT and StateT are pure, and technically they are. But let's be honest: if you have an application which is entirely living within a StateT, you're not getting the benefits of restrained mutation that you want from pure code. May as well call a spade a spade, and accept that you have a mutable variable.

这让我觉得情况确实如此:对于 IO 我们是诚实的,对于 StateT,我们对可变性并不诚实......但这似乎是另一个问题,而不是上面的法律是什么试图展示;毕竟,MonadUnliftIO 假设 IO。我无法从概念上理解 IO 比其他东西更具限制性。

更新 1

睡了(一些)之后,我仍然很困惑,但随着时间的推移,我逐渐不那么迷糊了。我为 IO 制定了法律证明。我意识到 README 中存在 id。特别是,

instance MonadUnliftIO IO where
  askUnliftIO = return (UnliftIO id)

所以 askUnliftIO 会出现在 return 一个 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 ())

回到规律,当在转换后的 monad (askUnliftIO) 上进行往返时,确实似乎是在说 monad m 中的状态没有发生变化,其中往返是 unLiftIO -> liftIO.

继续上面的例子,barIO :: IO (),所以如果我们做barIO >>= (u -> liftIO (unliftIO u m)),那么u :: IO ()unliftIO u == IO (),然后liftIO (IO ()) == IO ()。 **因此,由于一切基本上都是 id 的应用程序,我们可以看到没有任何状态发生变化,即使我们正在使用 IO。我认为,至关重要的是 a 中的值永远不会是 运行,也不会因为使用 askUnliftIO 而修改任何其他状态。如果是这样,那么就像 randomIO :: IO a 的情况一样,如果我们不对它进行 运行 askUnliftIO,我们将无法获得相同的值。 (下面的验证尝试 1)

但是,似乎我们可以对其他 Monad 做同样的事情,即使它们确实保持状态。但我也看到,对于某些单子,我们可能无法这样做。考虑一个人为的例子:每次我们访问包含在有状态 monad 中的类型 a 的值时,一些内部状态都会改变。

验证尝试 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)

"StateT is bad, IO is OK"

这不是本文的重点。这个想法是 MonadBaseControl 允许在存在并发和异常的情况下使用有状态的 monad 转换器进行一些令人困惑的(通常是不受欢迎的)行为。

finally :: StateT s IO a -> StateT s IO a -> StateT s IO a 就是一个很好的例子。如果您使用“StateT 将类型 s 的可变变量附加到 monad m” 的比喻,那么您可能期望终结器操作可以访问最近的 s 抛出异常时的值。

forkState :: StateT s IO a -> StateT s IO ThreadId 是另一个。您可能希望来自输入的状态修改会反映在原始线程中。

lol :: StateT Int IO [ThreadId]
lol = do
  for [1..10] $ \i -> do
    forkState $ modify (+i)

您可能希望 lol 可以重写(模性能)为 modify (+ sum [1..10])。但那是不对的。 forkState 的实现只是将初始状态传递给分叉线程,然后 永远无法检索任何状态修改 。 easy/common 对 StateT 的理解让你失望了。

相反,您必须将 StateT s m a 视为 "a transformer that provides a thread-local immutable variable of type s which is implicitly threaded through a computation, and it is possible to replace that local variable with a new value of the same type for future steps of the computation."(或多或少是 s -> m (a, s) 的冗长英语复述),有了这种理解,行为finally 变得更清楚了:它是一个局部变量,所以它不会在异常中存活。同样,forkState 变得更加清晰:它是一个线程局部变量,因此显然对不同线程的更改不会影响任何其他线程。

有时这是您想要的。但这通常不是人们在 IRL 中编写代码的方式,它常常让人感到困惑。

长期以来,生态系统中执行此 "lowering" 操作的默认选择是 MonadBaseControl,这有很多缺点:令人困惑的类型、难以实现实例、不可能派生实例,有时会混淆行为。情况不太好。

MonadUnliftIO 将事物限制为一组更简单的 monad 转换器,并且能够提供相对简单的类型、可派生的实例和始终可预测的行为。代价是ExceptTStateT等变压器无法使用

基本原则是:通过限制可能发生的事情,我们可以更容易地理解可能发生的事情。 MonadBaseControl 非常强大和通用,因此很难使用和混淆。 MonadUnliftIO 功能和通用性较差,但更易于使用。

So this appears to be saying that state is not mutated in the monad m when using askUnliftIO.

这不是真的 - 法律规定 unliftIO 除了将其降低到 IO 之外不应该对 monad 转换器做任何事情。这是违反该法律的事情:

newtype WithInt a = WithInt (ReaderT Int IO a)
  deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader Int)

instance MonadUnliftIO WithInt where
  askUnliftIO = pure (UnliftIO (\(WithInt readerAction) -> runReaderT 0 readerAction))

让我们验证一下这是否违反了给定的定律:askUnliftIO >>= (\u -> liftIO (unliftIO u m)) = m

test :: WithInt Int
test = do
  int <- ask
  print int
  pure int

checkLaw :: WithInt ()
checkLaw = do
  first <- test
  second <- askUnliftIO >>= (\u -> liftIO (unliftIO u test))
  when (first /= second) $
    putStrLn "Law violation!!!"

test返回的值和askUnliftIO ...lowering/lifting返回的值不一样,所以违法了。此外,观察到的效果也不同,这也不是很好。