web-dev-qa-db-ja.com

リーダーモナドの目的は何ですか?

リーダーモナドはとても複雑で、役に立たないようです。 JavaまたはC++のような命令型言語では、誤解しない限り、リーダーモナドに同等の概念はありません。

簡単な例を挙げて、これを少し明確にできますか?

110
chipbk10

怖がらないで!リーダーモナドは実際にはそれほど複雑ではなく、実際に使いやすいユーティリティを備えています。

モナドに近づくには2つの方法があります。

  1. モナドとは何ですかdo?どのような操作が装備されていますか?それは何のためにあるのですか?
  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人が 依存性注入 と呼ぶことを行うためにも使用されます。たとえば、 negamax アルゴリズムが頻繁に使用されます(非常に最適化されたフォーム)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

スポット価格を取得します。その後、コードでこの辞書を呼び出すことができます。それはうまくいきません!通貨辞書は不変であるため、プログラムの存続期間だけでなく、compiledを取得した時点から同じでなければなりません!それで、あなたは何をしますか?さて、1つのオプションはReaderモナドを使用することです:

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

おそらく最も古典的なユースケースは、インタープリターの実装です。しかし、それを見る前に、別の関数を導入する必要があります

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

さて、Haskellや他の関数型言語は lambda calculus に基づいています。ラムダ計算には次のような構文があります

 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 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つの方法は、質問することです。答えは、読者モナドは実際にはすべてのモナドの中で最もシンプルでエレガントなものの1つであるということです。

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

Readerは関数の単なる名です!すでにrunReaderを定義していますので、APIの他の部分はどうですか?まあ、すべてのMonadFunctorです:

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)

さて、読者モナドは単なる関数です。なぜReaderが必要なのですか?良い質問。実際には、あなたはそれを必要としません!

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

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

これらはさらに単純です。さらに、askは単にidであり、localは関数の順序が入れ替わった関数合成です!

150
Philip JF

Readerモナドのバリアントがどこでもであることを自分で発見するまで、あなたがそうだったように私は戸惑っていたことを覚えています。どうやって発見したの?なぜなら、私はコードの小さなバリエーションであることが判明したからです。

たとえば、ある時点でhistorical値を処理するコードを書いていました。時間とともに変化する値。これの非常に単純なモデルは、ある時点からその時点の値までの関数です。

import Control.Applicative

-- | A History with timeline type t and value type a.
newtype History t a = History { observe :: t -> a }

instance Functor (History t) where
    -- Apply a function to the contents of a historical value
    fmap f hist = History (f . observe hist)

instance Applicative (History t) where
    -- A "pure" History is one that has the same value at all points in time
    pure = History . const

    -- This applies a function that changes over time to a value that also 
    -- changes, by observing both at the same point in time.
    ff <*> fx = History $ \t -> (observe ff t) (observe fx t)

instance Monad (History t) where
    return = pure
    ma >>= f = History $ \t -> observe (f (observe ma t)) t

Applicativeインスタンスは、employees :: History Day [Person]およびcustomers :: History Day [Person]がある場合、これを実行できることを意味します。

-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers

つまり、FunctorApplicativeを使用すると、通常の非履歴関数を履歴に合わせて調整できます。

モナドインスタンスは、関数(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m cを考慮することで最も直感的に理解されます。 a -> History t b型の関数は、ab値の履歴にマップする関数です。たとえば、getSupervisor :: Person -> History Day SupervisorgetVP :: Supervisor -> History Day VPを使用できます。したがって、HistoryのMonadインスタンスは、これらのような関数の構成に関するものです。たとえば、getSupervisor >=> getVP :: Person -> History Day VPは、Personに対して、VPsの履歴を取得する関数です。

さて、このHistoryモナドは、実際にはexactlyReaderと同じです。 History t aReader t aと同じです(t -> aと同じです)。

別の例:最近、Haskellで [〜#〜] olap [〜#〜] のプロトタイプを作成しています。ここでの1つのアイデアは、「ハイパーキューブ」の概念です。これは、一連のディメンションの交点から値へのマッピングです。ああ、またか:

newtype Hypercube intersection value = Hypercube { get :: intersection -> value }

ハイパーキューブの一般的な操作の1つは、マルチキューブスカラー関数をハイパーキューブの対応するポイントに適用することです。これは、ApplicativeHypercubeインスタンスを定義することで取得できます。

instance Functor (Hypercube intersection) where
    fmap f cube = Hypercube (f . get cube)


instance Applicative (Hypercube intersection) where
    -- A "pure" Hypercube is one that has the same value at all intersections
    pure = Hypercube . const

    -- Apply each function in the @ff@ hypercube to its corresponding point 
    -- in @fx@.
    ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)

上記のHistoryコードをコピーして名前を変更しました。おわかりのように、HypercubeReaderです。

それは延々と続く。たとえば、このモデルを適用すると、言語インタープリターもReaderに要約されます。

  • 式= a Reader
  • 自由変数= askの使用
  • 評価環境= Reader実行環境。
  • バインド構成体= local

良い例えは、Reader r aが「穴」のあるaを表しているため、どのaについて話しているのか分からないということです。実際にaを取得できるのは、rを指定して穴を埋めてからです。そのようなものがたくさんあります。上記の例では、「履歴」は時間を指定するまで計算できない値、ハイパーキューブは交差点を指定するまで計算できない値、言語表現はできる値変数の値を指定するまで計算されません。また、Reader r ar -> aと同じである理由を直感的に示します。これは、このような関数も直感的にarが欠落しているためです。

したがって、FunctorApplicativeMonadおよびReaderインスタンスは、「aが欠落しているr」などのモデルを作成する場合に非常に有用な一般化であり、これらの「不完全な」オブジェクトを完全なものとして扱うことができます。

同じことを言う別の方法:Reader r arを消費してaを生成するものであり、FunctorApplicative、およびMonadインスタンスはReadersを操作するための基本的なパターンです。 Functor =別のReaderの出力を変更するReaderを作成します。 Applicative = 2つのReadersを同じ入力に接続し、それらの出力を結合します。 Monad = Readerの結果を検査し、それを使用して別のReaderを構築します。 localおよびwithReader関数=入力を別のReaderに変更するReaderを作成します。

52
Luis Casillas

JavaまたはC++では、問題なくどこからでも変数にアクセスできます。コードがマルチスレッドになると問題が発生します。

Haskellでは、ある関数から別の関数に値を渡す方法は2つしかありません。

  • 呼び出し可能関数の入力パラメーターの1つを介して値を渡します。欠点は次のとおりです。1)すべての変数をそのように渡すことはできません-入力パラメーターのリストはただ気になります。 2)関数呼び出しのシーケンス:fn1 -> fn2 -> fn3、関数fn2は、fn1からfn3に渡すパラメーターを必要としない場合があります。
  • 何らかのモナドのスコープで値を渡します。欠点は、モナドの概念が何であるかをしっかりと理解する必要があることです。値を渡すことは、Monadsを使用できる多くのアプリケーションの1つにすぎません。実際、モナドの構想は信じられないほど強力です。すぐに洞察を得られなかったとしても、動揺しないでください。試してみて、さまざまなチュートリアルを読んでください。あなたが得る知識は報われるでしょう。

Readerモナドは、関数間で共有したいデータを渡すだけです。関数はそのデータを読み取ることができますが、変更することはできません。 Readerモナドを実行するのはそれだけです。まあ、ほとんどすべて。 localのような関数も多数ありますが、初めてasksのみを使用できます。

19
Dmitry Bespalov