Я просмотрел 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)