我浏览了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
,我们也看不到任何状态被更改。至关重要的是,我认为重要的是,由于使用IO
,a
中的值永远不会运行,也不会修改任何其他状态。如果确实如此,那么就像在askUnliftIO
的情况下一样,如果不对它运行randomIO :: IO a
,我们将无法获得相同的值。 (下面的验证尝试1)
但是,看来我们可以对其他Monad做同样的事情,即使它们确实保持了状态。但我也看到,对于某些单子来说,我们可能无法做到。考虑一个人为的例子:每次我们访问有状态单子中包含的类型askUnliftIO
的值时,内部状态都会改变。
验证尝试1
a
到目前为止还不错,但是对于为什么会发生以下情况感到困惑:
> fooIO >> askUnliftIO
5
> fooIOunlift = fooIO >> askUnliftIO
> :t fooIOunlift
fooIOunlift :: IO (UnliftIO IO)
> fooIOunlift
5
答案 0 :(得分:7)
“ StateT不好,IO正常”
这不是本文的重点。这个想法是,MonadBaseControl
在存在并发和异常的情况下,允许有状态monad转换器产生一些令人困惑的(通常是不受欢迎的)行为。
finally :: StateT s IO a -> StateT s IO a -> StateT s IO a
是一个很好的例子。如果您使用“ StateT
将s
类型的可变变量附加到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转换器,并且能够提供相对简单的类型,可派生的实例以及始终可预测的行为。代价是MonadUnliftIO
,ExceptT
等变压器无法使用。
基本原理是:通过限制可能发生的事情,我们可以更轻松地理解可能发生的事情。 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
降低/提升的值不同,因此违反了法律。此外,观察到的效果是不同的,也不是很好。