1. ホーム
  2. haskell

[解決済み] リーダーモナドの目的は何ですか?

2022-05-24 02:39:41

質問

リーダーモナドはとても複雑で、使い物にならないように思えます。JavaやC++のような命令型言語では、私の記憶違いでなければ、リーダーモナドに相当する概念は存在しません。

簡単な例を挙げて、これを少しクリアにしてもらえませんか?

どのように解決するのですか?

怖がらないでください。 リーダーモナドは実はそれほど複雑ではなく、本当に使いやすいユーティリティを持っているのです。

モナドにアプローチする方法は2つあります。

  1. モナドは何をするのか する ? どのような機能を備えていますか? どんなことに使えるの?
  2. モナドはどのように実装されるのですか? どこから発生するのでしょうか?

最初のアプローチから、リーダーモナドは何らかの抽象的な型である

data Reader env a

そのような

-- Reader is a monad
instance Monad (Reader env)

-- and we have a function to get its environment
ask :: Reader env env

-- finally, we can run a Reader
runReader :: Reader env a -> env -> a

では、これをどう使うか? リーダーモナドは(暗黙の)設定情報を計算で渡すのに適しています。

計算の中に様々な場面で必要となる定数があり、本当は同じ計算を異なる値で実行できるようにしたい場合は、いつでもリーダーモナドを使用する必要があります。

リーダーモナドは、OOの人たちが言うところの 依存性注入 . 例えば ネガマックス アルゴリズムは、2人用ゲームにおけるポジションの値を計算するために、(高度に最適化された形で)頻繁に使用されています。 しかし、アルゴリズム自体は、ゲームにおいて次のポジションが何であるかを決定できる必要があることと、現在のポジションが勝利ポジションであるかどうかを判断できる必要があることを除いて、どのようなゲームをプレイしているかを気にすることはありません。

 import Control.Monad.Reader

 data GameState = NotOver | FirstPlayerWin | SecondPlayerWin | Tie

 data Game position
   = Game {
           getNext :: position -> [position],
           getState :: position -> GameState
          }

 getNext' :: position -> Reader (Game position) [position]
 getNext' position
   = do game <- ask
        return $ getNext game position

 getState' :: position -> Reader (Game position) GameState
 getState' position
   = do game <- ask
        return $ getState game position


 negamax :: Double -> position -> Reader (Game position) Double
 negamax color position
     = do state <- getState' position 
          case state of
             FirstPlayerWin -> return color
             SecondPlayerWin -> return $ negate color
             Tie -> return 0
             NotOver -> do possible <- getNext' position
                           values <- mapM ((liftM negate) . negamax (negate color)) possible
                           return $ maximum values

これは、有限で決定論的な2人用のゲームであれば、動作します。

このパターンは本当に依存性注入でないものにも有用です。例えば、あなたが金融の仕事をしていて、資産(例えばデリバティブ)に値段をつけるための複雑なロジックを設計しているとします。しかし、多通貨を扱うためにプログラムを変更することになります。 この場合、通貨間の変換が必要です。最初の試みは、トップレベルの関数を定義することです。

type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict

で、スポット価格を取得します。 そして、この辞書をコードで呼び出すことができます......が、待てよ! しかし、それはうまくいきません! 通貨辞書は不変なので、プログラムの寿命が尽きるまで同じである必要があります。 がコンパイルされたときから同じでなければなりません。 ! では、どうすればいいのでしょうか? まあ、1つの選択肢はReaderモナドを使うことでしょう。

 computePrice :: Reader CurrencyDict Dollars
 computePrice
    = do currencyDict <- ask
      --insert computation here

おそらく最も古典的な使用例は、インタプリタの実装でしょう。 しかし、それを見る前に、もう一つの機能である

 local :: (env -> env) -> Reader env a -> Reader env a

なるほど、Haskellをはじめとする関数型言語は ラムダ計算 . ラムダ計算の文法は次のようなものです。

 data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)

で、この言語のための評価器を書きたいと思います。そのためには、用語に関連するバインディングのリストである環境を追跡する必要があります(実際には、静的スコープを行いたいのでクロージャになるでしょう)。

 newtype Env = Env ([(String, Closure)])
 type Closure = (Term, Env)

終了したら、値(またはエラー)を出すようにします。

 data Value = Lam String Closure | Failure String

では、インタプリタを書いてみましょう。

interp' :: Term -> Reader Env Value
--when we have a lambda term, we can just return it
interp' (Lambda nv t)
   = do env <- ask
        return $ Lam nv (t, env)
--when we run into a value, we look it up in the environment
interp' (Var v)
   = do (Env env) <- ask
        case lookup (show v) env of
          -- if it is not in the environment we have a problem
          Nothing -> return . Failure $ "unbound variable: " ++ (show v)
          -- if it is in the environment, then we should interpret it
          Just (term, env) -> local (const env) $ interp' term
--the complicated case is an application
interp' (Apply t1 t2)
   = do v1 <- interp' t1
        case v1 of
           Failure s -> return (Failure s)
           Lam nv clos -> local (\(Env ls) -> Env ((nv, clos) : ls)) $ interp' t2
--I guess not that complicated!

最後に、些細な環境を渡すことで使用できるようになります。

interp :: Term -> Value
interp term = runReader (interp' term) (Env [])

というわけで、これで完成です。 ラムダ計算のための完全に機能するインタプリタです。


このことについて考えるもう一つの方法は、「どのように実装されているのか」ということです。 その答えは、リーダーモナドは実はすべてのモナドの中で最もシンプルでエレガントなものの1つであるということです。

newtype Reader env a = Reader {runReader :: env -> a}

リーダーとは、単に関数の洒落た呼び方です 私たちはすでに runReader を定義しました。では、APIの他の部分はどうなっているのでしょうか? さて、すべての Monad はまた Functor :

instance Functor (Reader env) where
   fmap f (Reader g) = Reader $ f . g

さて、モナドを取得するために

instance Monad (Reader env) where
   return x = Reader (\_ -> x)
   (Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x

というのは、それほど怖いものではありません。 ask は本当にシンプルです。

ask = Reader $ \x -> x

一方 local はそれほど悪くはない。

local f (Reader g) = Reader $ \x -> runReader g (f x)

なるほど、リーダーモナドは単なる関数なんですね。なぜリーダーを持つのでしょうか?いい質問ですね。実は、必要ないんです!

instance Functor ((->) env) where
  fmap = (.)

instance Monad ((->) env) where
  return = const
  f >>= g = \x -> g (f x) x

これらはさらにシンプルです。さらに ask は単に id であり local は関数の順番を入れ替えただけの関数合成です!