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


122

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

簡単な例を挙げて、これを少し明確にしていただけますか?


21
(変更不可能な)環境から一部の値を読み取りたいが、その環境を明示的に渡したくない場合は、リーダーモナドを使用します。JavaまたはC ++では、グローバル変数を使用します(まったく同じではありません)。
ダニエルフィッシャー

5
@ダニエル:それは答えの
SingleNegationElimination

@TokenMacGuy答えが短すぎます。もう少し長く考えるのは遅すぎます。他に誰もいない場合は、私が眠った後にします。
ダニエルフィッシャー

8
JavaまたはC ++では、Readerモナドは、コンストラクターでオブジェクトに渡される構成パラメーターに類似しており、オブジェクトの存続期間中に変更されることはありません。Clojureでは、パラメーターとして明示的に渡す必要なしに関数の動作をパラメーター化するために使用される動的スコープ変数のようなものです。
danidiaz 2013年

回答:


169

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

モナドに近づく方法は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

それでは、これをどのように使用しますか?さて、readerモナドは、計算を通じて(暗黙の)構成情報を渡すのに適しています。

さまざまな時点で必要な計算に「定数」があるが、実際には異なる値で同じ計算を実行したい場合は、リーダーモナドを使用する必要があります。

リーダーモナドは、オブジェクト指向の人々が依存性注入と呼ぶものを実行するためにも使用されます。たとえば、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

スポット価格を取得します。その後、コードでこのディクショナリを呼び出すことができます。それはうまくいきません!通貨ディクショナリは不変なので、プログラムの存続期間だけでなく、コンパイルされるときから同じでなければなりません。それで、あなたは何をしますか?まあ、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つの方法は、次のように質問することです。答えは、リーダーモナドは実際にはすべてのモナドの中で最も単純で最もエレガントなものの1つであるということです。

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

リーダーは、関数のファンシーな名前です!すでに定義している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ただでidlocal切り替え機能のためにだけ関数合成です!


6
非常に興味深い答え。正直なところ、モナドを確認したいときに何度も読みました。ちなみに、nagamaxアルゴリズムについては、「値<-mapM(negate。negamax(negate color))可能」が正しくないようです。あなたが提供するコードは、読者モナドがどのように機能するかを示すためだけのものです。しかし、時間があれば、negamaxアルゴリズムのコードを修正できますか?なぜなら、読者モナドを使ってネガマックスを解くのは興味深いからです。
chipbk10 2013

4
それでReader、モナド型クラスの特定の実装を持つ関数ですか?以前にそれを言うことは私が少し戸惑うのを助けたでしょう。最初は私はそれを取得していませんでした。途中で、「ああ、不足している値を指定すると、望ましい結果が得られるものを返すことができる」と思った。それは便利だと思いましたが、関数がこれを正確に行うことに突然気づきました。
ziggystar 14

1
これを読んだ後、私はそれのほとんどを理解しています。local機能は、しかし、いくつかのより詳細な説明が必要ない...
クリストフ・デ・トロイヤーに

@フィリップモナドインスタンスについて質問があります。bind関数を次のように記述できません(Reader f) >>= g = (g (f x))か?
zeronone 2016年

@zerononeはどこにありxますか?
Ashish Negi

56

読者モナドの亜種が至る所にあることを自分で発見するまで、あなたがそうであったように困惑したことを覚えています。どうやって見つけたの?小さなバリエーションであることが判明したコードを書き続けたからです。

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

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マップする関数です。たとえば、あなたが持っている可能性があり、そして。したがって、のモナドインスタンスは、次のような関数の作成に関するものです。たとえば、は、すべてのについて、それらが持っていたの履歴を取得する関数です。abgetSupervisor :: Person -> History Day SupervisorgetVP :: Supervisor -> History Day VPHistorygetSupervisor >=> getVP :: Person -> History Day VPPersonVP

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

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

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

ハイパーキューブでの操作の一般的なものの1つは、ハイパーキューブの対応する点に複数の場所のスカラー関数を適用することです。これは、次のApplicativeインスタンスを定義することで取得できますHypercube

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上記のコードをコピーペーストして名前を変更しました。あなたが言うことができるように、HypercubeまたですReader

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

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

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

したがってFunctorApplicativeとのMonadインスタンスは、「が欠けているReaderもの」のようなものをモデル化する場合に非常に便利な一般化であり、これらの「不完全な」オブジェクトを完全であるかのように扱うことができます。ar

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


5
すばらしい答えです。また、使用することができますGeneralizedNewtypeDeriving導き出すために拡張子をFunctorApplicativeMonadその基礎となる型に基づいnewtypesのために、など。
Rein Henrichs、2014年

20

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

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

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

Readerモナドは、関数間で共有したいデータを渡すだけです。関数はそのデータを読み取ることができますが、変更することはできません。これで、Readerモナドの処理は完了です。まあ、ほとんどすべて。のような機能もたくさんありますlocalが、はじめてこだわることができますasks


3
モナドを使用して暗黙的にデータを渡すことのもう1つの欠点は、多くの「命令型」コードをdo-notationで記述していることを非常に簡単に見つけることができることです。
ベンジャミンホジソン

4
@BenjaminHodgson do表記でモナドを使用して「命令型の」コードを記述することは、必ずしも副次的(純粋でない)コードを記述することを意味しません。実際、Haskellの副次的なコードはIOモナド内でのみ可能です。
ドミトリー・ベスパロフ2014年

他の関数がwhere句によってその関数に付加されている場合、それは変数を渡す3番目の方法として受け入れられますか?
Elmex80s
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.