リーダーモナドはとても複雑で役に立たないようです。JavaやC++のような命令型言語では、私が間違っていなければ、リーダーモナドに相当する概念はありません。
簡単な例を挙げて、これを少し明確にしていただけますか?
リーダーモナドはとても複雑で役に立たないようです。JavaやC++のような命令型言語では、私が間違っていなければ、リーダーモナドに相当する概念はありません。
簡単な例を挙げて、これを少し明確にしていただけますか?
怖がらないで!リーダーモナドは実際にはそれほど複雑ではなく、本当に使いやすいユーティリティを備えています。
モナドにアプローチする方法は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
スポット価格を取得します。その後、コードでこの辞書を呼び出すことができます....しかし待ってください!それはうまくいきません!通貨辞書は不変であるため、プログラムの存続期間中だけでなく、コンパイルされたときから同じである必要があります。それで、あなたは何をしますか?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}
Readerは、関数の単なる名前です。すでに定義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
リーダーモナドの変種がいたるところにあることを自分で発見するまで、私はあなたがそうであったように戸惑ったことを覚えています。どうやってそれを発見したのですか?小さなバリエーションであることが判明したコードを書き続けたからです。
たとえば、ある時点で、履歴値を処理するためのコードを書いていました。時間の経過とともに変化する値。これの非常に単純なモデルは、ある時点からその時点の値までの関数です。
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
つまり、履歴を処理するために、通常の非履歴機能を適応させることができますFunctor
。Applicative
モナドインスタンスは、関数を検討することで最も直感的に理解できます(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
。タイプの関数は、を値の履歴にa -> History t b
マップする関数です。たとえば、、、およびを使用できます。したがって、のモナドインスタンスは、このような関数を作成することです。たとえば、は、彼らが持っていたsの履歴を取得する関数です。a
b
getSupervisor :: Person -> History Day Supervisor
getVP :: Supervisor -> History Day VP
History
getSupervisor >=> getVP :: Person -> History Day VP
Person
VP
まあ、このHistory
モナドは実際にはとまったく同じReader
です。 History t a
は実際にはと同じReader 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
このモデルを適用すると、言語通訳者もに要約されます。
Reader
ask
Reader
実行環境。local
良い例えとして、aは「穴」が含まれていることをReader r a
表しており、これにより、私たちが話しているa
ことを知ることができなくなります。穴を埋めるためにを提供した場合にa
のみ、実際の値を取得できます。そのようなものはたくさんあります。上記の例では、「履歴」は時間を指定するまで計算できない値であり、ハイパーキューブは交差点を指定するまで計算できない値であり、言語式は次のことができる値です。変数の値を指定するまで計算されません。また、このような関数には直感的にが欠落しているため、なぜがと同じであるかについての直感も得られます。a
r
Reader r a
r -> a
a
r
したがって、、Functor
およびApplicative
のMonad
インスタンスは、「が欠落しているReader
」種類のものをモデル化する場合に非常に便利な一般化であり、これらの「不完全な」オブジェクトを完全であるかのように扱うことができます。a
r
同じことを言うさらに別の言い方をすると、aReader r a
はを消費r
して生成するものでa
ありFunctor
、インスタンスはsを操作するための基本的なパターンです。 =別の出力を変更するaを作成します; = 2つのを同じ入力に接続し、それらの出力を結合します。= aの結果を調べ、それを使用して別のを作成します。and関数=makeは、入力を別のに変更します。Applicative
Monad
Reader
Functor
Reader
Reader
Applicative
Reader
Monad
Reader
Reader
local
withReader
Reader
Reader
JavaまたはC++では、どこからでも問題なく任意の変数にアクセスできます。コードがマルチスレッドになると、問題が発生します。
Haskellでは、ある関数から別の関数に値を渡す方法は2つしかありません。
fn1 -> fn2 -> fn3
、関数は、に渡すパラメータをfn2
必要としない場合があります。fn1
fn3
Readerモナドは、関数間で共有するデータを渡すだけです。関数はそのデータを読み取ることができますが、変更することはできません。Readerモナドを実行するのはこれだけです。まあ、ほとんどすべて。のような機能もたくさんありますがlocal
、初めてasks
だけに固執することができます。