如何理解"MonadUnliftIO"对"无状态monad"的要求?

bba*_*ker 13 haskell monad-transformers classy-prelude

我已经浏览了https://www.fpcomplete.com/blog/2017/06/tale-of-two-brackets,虽然略读了一些部分,但我仍然不太明白核心问题" StateT不好,IO是"好吧",除了模糊地感觉到Haskell允许一个人编写糟糕的StateTmonad(或者在文章的最终例子中,MonadBaseControl而不是StateT,我认为).

在黑线鳕中,必须满足以下法律:

askUnliftIO >>= (\u -> liftIO (unliftIO u m)) = m
Run Code Online (Sandbox Code Playgroud)

所以这似乎是说m在使用时状态不会在monad中发生变异askUnliftIO.但在我看来,IO整个世界都可以成为国家.例如,我可以读写磁盘上的文本文件.

引用迈克尔的另一篇文章,

虚假纯度我们说WriterT和StateT是纯粹的,技术上它们是纯粹的.但是说实话:如果你有一个完全生活在StateT中的应用程序,你就不会从纯代码中获得你想要的限制突变的好处.也可以称之为铁锹,并接受你有一个可变变量.

这让我觉得情况确实如此:对于IO我们是诚实的,StateT我们并不诚实可靠......但这似乎是上述法律试图表明的另一个问题; 毕竟,MonadUnliftIO是假设IO.我在概念上理解如何IO比其他东西更具限制性.

更新1

睡觉后(有些人),我仍然感到困惑,但随着时间的推移逐渐减少.我制定了法律证据IO.我意识到idREADME中的存在.特别是,

instance MonadUnliftIO IO where
  askUnliftIO = return (UnliftIO id)
Run Code Online (Sandbox Code Playgroud)

所以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 ())
Run Code Online (Sandbox Code Playgroud)

回到法律,它实际上似乎是说m当在转换的monad(askUnliftIO)上进行往返时,状态在monad中没有变异,其中往返是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)

但是,看起来我们仍然可以为其他Monads做同样的事情,即使他们确实保持状态.但我也看到,对于一些单子,我们可能无法这样做.考虑一个人为的例子:每当我们访问a有状态monad中包含的类型的值时,一些内部状态就会改变.

验证尝试1

> fooIO >> askUnliftIO
5
> fooIOunlift = fooIO >> askUnliftIO
> :t fooIOunlift
fooIOunlift :: IO (UnliftIO IO)
> fooIOunlift
5
Run Code Online (Sandbox Code Playgroud)

到目前为止很好,但是为什么发生以下情况感到困惑:

 > 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)
Run Code Online (Sandbox Code Playgroud)

eph*_*ion 7

“ StateT不好,IO可以”

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

finally :: StateT s IO a -> StateT s IO a -> StateT s IO a是一个很好的例子。如果使用“ StateT将类型的可变变量附加s到monad m”隐喻上,则可能会期望s在引发异常时,finalizer操作可以访问最新值。

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

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

您可能希望lol可以将其重写为(模性能)modify (+ sum [1..10])。但这是不对的。的实现forkState只是将初始状态传递给派生线程,然后再也无法检索任何状态修改。对这里的简单/共同理解使StateT您失望。

取而代之的是,您必须采用更细微的观点,StateT s m a例如“提供了s通过计算隐式线程化的线程局部不可变类型变量的转换器,并且可以用相同类型的新值替换该局部变量。以便将来进行计算。” (或多或少的英语复述s -> m (a, s))了解了这一点后,的行为finally变得更加清晰:这是一个局部变量,因此它无法在异常中幸免。同样,forkState变得更加清晰:这是线程局部变量,因此显然更改为其他线程不会影响任何其他线程。

有时这就是您想要的。但这通常不是人们编写IRL代码的方式,并且常常使人们感到困惑。

长期以来,生态系统中执行此“降低”操作的默认选择是MonadBaseControl,这有很多弊端:令人困惑的类型,难以实现的实例,无法派生的实例,有时会使行为令人困惑。情况不是很好。

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

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

因此,这似乎是在使用askUnliftIO时单子状态m不会发生变化。

这不是事实-法律规定,unliftIO除了将monad变压器降低到内之外,不应对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))
Run Code Online (Sandbox Code Playgroud)

让我们验证这是否违反了给定的定律: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!!!"
Run Code Online (Sandbox Code Playgroud)

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