Как понять требование MonadUnliftIO о том, что "монады без состояния" не нужны?
Я просмотрел 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
Весь мир может быть государством. Я мог бы, например, читать и писать в текстовый файл на диске.
Процитирую еще одну статью Майкла,
Ложная чистота Мы говорим, что WriterT и StateT чисты, и технически это так. Но давайте будем честными: если у вас есть приложение, которое полностью живет в StateT, вы не получите тех преимуществ сдержанной мутации, которые вам нужны от чистого кода. Можно также назвать вещи своими именами и признать, что у вас есть изменяемая переменная.
Это заставляет меня думать, что это действительно так: с IO мы честны, с StateT
мы не честны в отношении изменчивости... но это кажется другой проблемой, чем то, что пытается показать закон выше; в конце концов, MonadUnliftIO
предполагает IO
, У меня проблемы с пониманием концептуально, как IO
является более строгим, чем что-то еще.
Обновление 1
После сна (немного) я все еще в замешательстве, но постепенно становлюсь все меньше, так как день идет. Я разработал законное доказательство для IO
, Я понял наличие id
в README. Особенно,
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
), где туда и обратно 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 ниже)
Но, похоже, мы могли бы сделать то же самое для других монад, даже если они поддерживают состояние. Но я также вижу, что для некоторых монад мы не можем этого сделать. Думая о надуманном примере: каждый раз, когда мы получаем доступ к значению типа 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)
0 ответов
"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 ...
Понижение / подъем разные, поэтому закон нарушен. Кроме того, наблюдаемые эффекты различны, что тоже не очень.