如何理解“ MonadUnliftIO”对“无状态单子”的要求?

时间:2019-02-20 04:47:00

标签: haskell classy-prelude

我浏览了https://www.fpcomplete.com/blog/2017/06/tale-of-two-brackets,尽管略读了一些部分,但我仍然不太了解核心问题“ StateT不好,IO不错”,除了朦胧地感觉到Haskell允许人们编写糟糕的StateT单子(或者,在本文的最终示例中,我认为MonadBaseControl而不是StateT。)

在围场中,必须满足以下法律:

askUnliftIO >>= (\u -> liftIO (unliftIO u m)) = m

因此,这似乎是在使用m时单子askUnliftIO中的状态未发生突变。但是在我看来,在IO中,整个世界都是国家。例如,我可能正在读写磁盘上的文本文件。

要引用another article by Michael

  

错误的纯度我们说WriterT和StateT是纯净的,从技术上讲它们是纯净的   是。但是说实话:如果您的应用程序完全是   住在StateT内,您没有得到克制的好处   您想要从纯代码中获得的变异。也可以称锹   铲除,并接受您具有可变变量。

这使我认为情况确实如此:在IO方面,我们很诚实,在StateT方面,我们对可变性并不诚实...但这似乎是上述法律所试图解决的另一个问题节目;毕竟,MonadUnliftIO假设IO。我在概念上难以理解IO的限制性比其他事物更严格。

更新1

(有些)睡觉后,我仍然感到困惑,但是随着一天的累累,我逐渐变得越来越少。我为IO制定了法律证明。我意识到自述文件中存在id。特别是

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)上进行往返时,单子askUnliftIO的状态不会发生突变,其中往返为{ {1}}-> unLiftIO

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

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

验证尝试1

a

到目前为止还不错,但是对于为什么会发生以下情况感到困惑:

> fooIO >> askUnliftIO
5
> fooIOunlift = fooIO >> askUnliftIO
> :t fooIOunlift
fooIOunlift :: IO (UnliftIO IO)
> fooIOunlift
5

1 个答案:

答案 0 :(得分:7)

  

“ StateT不好,IO正常”

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

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

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的简单/共同理解使您无法做到。

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

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

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

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

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

  

因此,这似乎是在使用askUnliftIO时单子m中的状态没有发生突变。

这是不正确的-法律规定MonadUnliftIO除了将monad变压器降低到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!!!" 返回的值和test降低/提升的值不同,因此违反了法律。此外,观察到的效果是不同的,也不是很好。