Я просмотрел 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 чисты, и технически они находятся. Но давайте будем честными: если у вас есть приложение, которое полностью живя в государстве, вы не получаете преимуществ от мутация, которую вы хотите от чистого кода. Можно также назвать вещи своими именами spade, и примите, что у вас есть изменяемая переменная.
Это заставляет меня думать, что это действительно так: с 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)