如何理解 `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
中,整个世界都可以是状态。例如,我可以读取和写入磁盘上的文本文件。
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 转换器,并且能够提供相对简单的类型、可派生的实例和始终可预测的行为。代价是ExceptT
、StateT
等变压器无法使用
基本原则是:通过限制可能发生的事情,我们可以更容易地理解可能发生的事情。 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返回的值不一样,所以违法了。此外,观察到的效果也不同,这也不是很好。
我看了 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
中,整个世界都可以是状态。例如,我可以读取和写入磁盘上的文本文件。
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 转换器,并且能够提供相对简单的类型、可派生的实例和始终可预测的行为。代价是ExceptT
、StateT
等变压器无法使用
基本原则是:通过限制可能发生的事情,我们可以更容易地理解可能发生的事情。 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返回的值不一样,所以违法了。此外,观察到的效果也不同,这也不是很好。