Ответ 1
"StateT плох, IO в порядке"
Это не совсем суть статьи. Идея состоит в том, что MonadBaseControl
допускает некоторые запутанные (и часто нежелательные) поведения с монадными преобразователями с состоянием при наличии параллелизма и исключений.
finally :: StateT s IO a -> StateT s IO a -> StateT s IO a
отличный пример. Если вы используете "StateT
присоединяет изменяемую переменную типа s
к метафоре монады 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
просто передает начальное состояние разветвленному потоку, а затем никогда не может получить какие-либо изменения состояния. Простое/общее понимание StateT
подводит вас здесь.
Вместо этого вы должны принять более нюансированное представление StateT s m a
как "преобразователь, который предоставляет локальную для потока неизменяемую переменную типа s
, которая неявно пронизывается через вычисления, и можно заменить эту локальную переменную на новое значение того же типа для будущих шагов вычисления. " (более или менее подробный английский пересказ s -> m (a, s)
). С этим пониманием поведение finally
становится немного более ясным: это локальная переменная, поэтому она не переживает исключений. Аналогично, forkState
становится более понятным: это локальная переменная потока, поэтому очевидно, что изменение другого потока не повлияет на другие.
Это иногда то, что вы хотите. Но обычно это не то, как люди пишут код IRL, и это часто смущает людей.
В течение долгого времени в экосистеме по умолчанию выполнялась эта операция "понижения" MonadBaseControl
, и у нее было множество недостатков: хелло-запутанные типы, сложные для реализации экземпляров, невозможные для получения экземпляров, иногда запутанное поведение. Не очень хорошая ситуация.
MonadUnliftIO
ограничивает вещи более простым набором монадных преобразователей и может обеспечить относительно простые типы, производные экземпляры и всегда предсказуемое поведение. Стоимость заключается в том, что трансформаторы ExceptT
, StateT
и т.д. Не могут его использовать.
Основной принцип: ограничивая то, что возможно, мы облегчаем понимание того, что может произойти. MonadBaseControl
чрезвычайно мощный и общий, и в результате довольно сложный в использовании и запутывающий. MonadUnliftIO
менее мощный и общий, но его гораздо проще использовать.
Таким образом, это говорит о том, что состояние не изменяется в монаде m при использовании askUnliftIO.
Это неправда - закон гласит, что unliftIO
не должен ничего делать с монадным трансформатором, кроме как понижать его в 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
и опусканием/поднятием askUnliftIO ...
, различается, поэтому закон нарушается. Кроме того, наблюдаемые эффекты различны, что тоже не очень.