web-dev-qa-db-ja.com

モナド変換子によるリフトの回避

IO上のモナド変換子のスタック(または1つのモナド変換子)に問題があります。すべてのアクションの前にリフトを使用することはひどく迷惑であることを除いて、すべてが良いです!それについては何の関係もないのではないかと思いますが、とにかく聞いてみようと思いました。

ブロック全体を持ち上げることは知っていますが、コードが実際に混合型である場合はどうなりますか? GHCが構文糖衣構文を投入した場合、それは素晴らしいことではないでしょうか(たとえば、<-$ = <- lift)?

49
aelguindy

すべての標準 mtl モナドの場合、liftはまったく必要ありません。 getputasktell —これらはすべて、スタックのどこかに適切なトランスフォーマーを備えた任意のモナドで機能します。欠落している部分はIOであり、そこにさえliftIOは任意のIOアクションを任意の数のレイヤーに持ち上げます。

これは、提供される各「効果」の型クラスを使用して行われます。たとえば、 MonadStategetputを提供します。トランスフォーマースタックの周りに独自のnewtypeラッパーを作成する場合は、GeneralizedNewtypeDeriving拡張子を使用してderiving (..., MonadState MyState, ...)を実行するか、独自のインスタンスをロールします。

instance MonadState MyState MyMonad where
  get = MyMonad get
  put s = MyMonad (put s)

これを使用して、一部のインスタンスを定義し、他のインスタンスを定義しないことで、結合されたトランスフォーマーのコンポーネントを選択的に公開または非表示にすることができます。

(独自の型クラスを定義し、標準のトランスフォーマーにボイラープレートインスタンスを提供することで、このアプローチを自分で定義したまったく新しいモナド効果に簡単に拡張できますが、まったく新しいモナドはまれです。ほとんどの場合、簡単に実行できます。 mtlが提供する標準セットを構成します。)

55
ehird

具象モナドスタックの代わりに型クラスを使用することで、関数をモナドに依存しないようにすることができます。

たとえば、次のような関数があるとします。

_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つあります。 IOmtlの一部ではないため、デフォルトではトランスフォーマー(IOTなど)も便利な型クラスもありません。では、IOアクションを恣意的に持ち上げたい場合はどうすればよいですか?

救助に来る MonadIOMonadStateMonadReaderなどとほぼ同じように動作しますが、唯一の違いは、持ち上げメカニズムがわずかに異なることです。これは次のように機能します。任意のIOアクションを実行し、liftIOを使用してモナドに依存しないバージョンに変換できます。そう:

_action :: IO ()
liftIO action :: MonadIO m => m ()
_

このように使用したいすべてのモナドアクションを変換することにより、面倒な持ち上げをすることなく、モナドを好きなだけ絡み合わせることができます。

48
dflemstr