IO
上のモナド変換子のスタック(または1つのモナド変換子)に問題があります。すべてのアクションの前にリフトを使用することはひどく迷惑であることを除いて、すべてが良いです!それについては何の関係もないのではないかと思いますが、とにかく聞いてみようと思いました。
ブロック全体を持ち上げることは知っていますが、コードが実際に混合型である場合はどうなりますか? GHCが構文糖衣構文を投入した場合、それは素晴らしいことではないでしょうか(たとえば、<-$
= <- lift
)?
すべての標準 mtl モナドの場合、lift
はまったく必要ありません。 get
、put
、ask
、tell
—これらはすべて、スタックのどこかに適切なトランスフォーマーを備えた任意のモナドで機能します。欠落している部分はIO
であり、そこにさえliftIO
は任意のIOアクションを任意の数のレイヤーに持ち上げます。
これは、提供される各「効果」の型クラスを使用して行われます。たとえば、 MonadState
はget
とput
を提供します。トランスフォーマースタックの周りに独自のnewtype
ラッパーを作成する場合は、GeneralizedNewtypeDeriving
拡張子を使用してderiving (..., MonadState MyState, ...)
を実行するか、独自のインスタンスをロールします。
instance MonadState MyState MyMonad where
get = MyMonad get
put s = MyMonad (put s)
これを使用して、一部のインスタンスを定義し、他のインスタンスを定義しないことで、結合されたトランスフォーマーのコンポーネントを選択的に公開または非表示にすることができます。
(独自の型クラスを定義し、標準のトランスフォーマーにボイラープレートインスタンスを提供することで、このアプローチを自分で定義したまったく新しいモナド効果に簡単に拡張できますが、まったく新しいモナドはまれです。ほとんどの場合、簡単に実行できます。 mtlが提供する標準セットを構成します。)
具象モナドスタックの代わりに型クラスを使用することで、関数をモナドに依存しないようにすることができます。
たとえば、次のような関数があるとします。
_bangMe :: State String ()
bangMe = do
str <- get
put $ str ++ "!"
-- or just modify (++"!")
_
もちろん、トランスフォーマーとしても機能することを理解しているので、次のように書くことができます。
_bangMe :: Monad m => StateT String m ()
_
ただし、別のスタックを使用する関数、たとえばReaderT [String] (StateT String IO) ()
などがある場合は、恐ろしいlift
関数を使用する必要があります。では、それはどのように回避されますか?
秘訣は、関数のシグネチャをさらに一般的にして、State
モナドがモナドスタックのどこにでも出現できるようにすることです。これは次のように行われます。
_bangMe :: MonadState String m => m ()
_
これにより、m
は、モナドスタック内の任意の場所で(事実上)状態をサポートするモナドになります。したがって、この関数は、そのようなスタックを持ち上げることなく機能します。
ただし、問題が1つあります。 IO
はmtl
の一部ではないため、デフォルトではトランスフォーマー(IOT
など)も便利な型クラスもありません。では、IOアクションを恣意的に持ち上げたい場合はどうすればよいですか?
救助に来る MonadIO
! MonadState
、MonadReader
などとほぼ同じように動作しますが、唯一の違いは、持ち上げメカニズムがわずかに異なることです。これは次のように機能します。任意のIO
アクションを実行し、liftIO
を使用してモナドに依存しないバージョンに変換できます。そう:
_action :: IO ()
liftIO action :: MonadIO m => m ()
_
このように使用したいすべてのモナドアクションを変換することにより、面倒な持ち上げをすることなく、モナドを好きなだけ絡み合わせることができます。