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

标签 haskell monad-transformers classy-prelude

我看过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 似乎会在 UnliftIO m 上返回 IO (IO a)

Prelude> fooIO = print 5
Prelude> :t fooIO
fooIO :: IO ()
Prelude> let barIO :: IO(IO ()); barIO = return fooIO
Prelude> :t barIO
barIO :: IO (IO ())

回到定律,它实际上似乎是在说,当在转换后的 monad 上进行往返时,monad m 中的状态不会发生突变 (askUnliftIO),其中往返行程为 unLiftIO -> liftIO

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

但是,我们似乎仍然可以对其他 Monad 做同样的事情,即使它们确实保持状态。但我也看到,对于某些单子(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 是另一个。您可能期望输入的状态修改将反射(reflect)在原始线程中。

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

您可能期望 lol 可以重写(模性能)为 modify (+ sum [1..10])。但那是不对的。 forkState 的实现只是将初始状态传递给 fork 线程,然后永远无法检索任何状态修改。对 StateT 的简单/常见理解在这里让你失败。

相反,您必须采用 StateT s m a 更细致的观点,将其视为“提供 s 类型的线程局部不可变变量的转换器,该变量通过隐式线程化计算,并且可以用相同类型的新值替换该局部变量以用于 future 的计算步骤。” (或多或少是对 s -> m (a, s) 的详细英语复述)有了这种理解,finally 的行为就变得更加清晰了:它是一个局部变量,因此它不会在异常中生存。同样,forkState 变得更加清晰:它是一个线程局部变量,因此显然对不同线程的更改不会影响任何其他线程。

有时就是您想要的。但这通常不是人们在现实生活中编写代码的方式,而且常常让人们感到困惑。

长期以来,生态系统中执行此“降低”操作的默认选择是 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 除了将 monad 转换器降低到 IO 之外不应该对它做任何事情。以下是违反该法律的内容:

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!!!"

testaskUnliftIO ... 降低/提升返回的值不同,因此违反了法律。而且,观察到的效果也不同,也不是很好。

关于haskell - 如何理解 `MonadUnliftIO`对 "no stateful monads"的要求?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54779029/

相关文章:

haskell - 为什么 Tuple 没有 Monad 实例?

haskell - 优雅前奏曲(头.头)

sqlite - 使用 Scotty 和 Sqlite 并在 Monad 方面遇到一些麻烦

haskell - 隐式类型强制?

haskell - 使用 Control.Concurrent.MonadIO 进行管道和 fork

haskell - 使用 runReaderT 消除 MonadReader 约束

haskell - 如何使用异常处理在 Haskell 中编写 "retryForever"函数?

parsing - 什么时候可以使用读取进行模棱两可的解析?

Haskell 似乎工作正常,但事实并非如此