Как понять требование 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 ... Понижение / подъем разные, поэтому закон нарушен. Кроме того, наблюдаемые эффекты различны, что тоже не очень.

Другие вопросы по тегам