독자 모나드의 목적은 무엇입니까?
독자 모나드는 너무 복잡해서 쓸모없는 것 같습니다. Java 또는 C ++과 같은 명령형 언어에서는 내가 실수하지 않으면 독자 모나드에 대한 동등한 개념이 없습니다.
간단한 예를 들어 주시고 약간 정리해 주실 수 있습니까?
겁내지 마! 리더 모나드는 실제로 그렇게 복잡하지 않으며 사용하기 쉬운 유틸리티를 가지고 있습니다.
모나드에 접근하는 방법에는 두 가지가 있습니다.
- 모나드 는 무엇을합니까? 어떤 작업이 장착되어 있습니까? 무엇이 좋은가요?
- 모나드는 어떻게 구현됩니까? 어디서 발생합니까?
첫 번째 접근 방식에서 독자 모나드는 추상적 유형입니다.
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 사람들이 의존성 주입 이라고 부르는 것을하기 위해 사용된다 . 예를 들어, 네가 막스 알고리즘은 두 플레이어 게임에서 위치 값을 계산하기 위해 자주 (고도로 최적화 된 형태로) 사용됩니다. 알고리즘 자체는 게임에서 "다음"위치를 결정할 수 있어야하고 현재 위치가 승리 위치인지 알 수 있어야한다는 점을 제외하고는 어떤 게임을하든 상관하지 않습니다.
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 인용 게임에서 작동합니다.
This pattern is useful even for things that are not really dependency injection. Suppose you work in finance, you might design some complicated logic for pricing an asset (a derivative say), which is all well and good and you can do without any stinking monads. But then, you modify your program to deal with multiple currencies. You need to be able to convert between currencies on the fly. Your first attempt is to define a top level function
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
to get spot prices. You can then call this dictionary in your code....but wait! That won't work! The currency dictionary is immutable and so has to be the same not only for the life of your program, but from the time it gets compiled! So what do you do? Well, one option would be to use the Reader monad:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
Perhaps the most classic use-case is in implementing interpreters. But, before we look at that, we need to introduce another function
local :: (env -> env) -> Reader env a -> Reader env a
Okay, so Haskell and other functional languages are based on the lambda calculus. Lambda calculus has a syntax that looks like
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
and we want to write an evaluator for this language. To do so, we will need to keep track of an environment, which is a list of bindings associated with terms (actually it will be closures because we want to do static scoping).
newtype Env = Env ([(String,Closure)])
type Closure = (Term, Env)
When we are done we should get out a value (or an error):
data Value = Lam String Closure | Failure String
So, lets write the interpreter:
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!
Finally, we can use it by passing a trivial environment:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
And that is it. A fully functional interpreter for the lambda calculus.
The other way to think about this is to ask: How is it implemented? The answer is that the reader monad is actually one of the simplest and most elegant of all monads.
newtype Reader env a = Reader {runReader :: env -> a}
Reader is just a fancy name for functions! We have already defined runReader
so what about the other parts of the API? Well, every Monad
is also a Functor
:
instance Functor (Reader env) where
fmap f (Reader g) = Reader $ f . g
Now, to get a monad:
instance Monad (Reader env) where
return x = Reader (\_ -> x)
(Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
which is not so scary. ask
is really simple:
ask = Reader $ \x -> x
while local
isn't so bad.
local f (Reader g) = Reader $ \x -> runReader g (f x)
Okay, so the reader monad is just a function. Why have Reader at all? Good question. Actually, you don't need it!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
These are even simpler. What's more, ask
is just id
and local
is just function composition with the order of the functions switched!
I remember being puzzled as you were, until I discovered on my own that variants of the Reader monad are everywhere. How did I discover it? Because I kept writing code that turned out to be small variations on it.
For example, at one point I was writing some code to deal with historical values; values that change over time. A very simple model of this is functions from points of time to the value at that point in time:
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
The Applicative
instance means that if you have employees :: History Day [Person]
and customers :: History Day [Person]
you can do this:
-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers
I.e., Functor
and Applicative
allow us to adapt regular, non-historical functions to work with histories.
The monad instance is most intuitively understood by considering the function (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
. A function of type a -> History t b
is a function that maps an a
to a history of b
values; for example, you could have getSupervisor :: Person -> History Day Supervisor
, and getVP :: Supervisor -> History Day VP
. So the Monad instance for History
is about composing functions like these; for example, getSupervisor >=> getVP :: Person -> History Day VP
is the function that gets, for any Person
, the history of VP
s that they've had.
Well, this History
monad is actually exactly the same as Reader
. History t a
is really the same as Reader t a
(which is the same as t -> a
).
Another example: I've been prototyping OLAP designs in Haskell recently. One idea here is that of a "hypercube," which is a mapping from intersections of a set of dimensions to values. Here we go again:
newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
One common of operation on hypercubes is to apply a multi-place scalar functions to corresponding points of a hypercube. This we can get by defining an Applicative
instance for 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)
I just copypasted the History
code above and changed names. As you can tell, Hypercube
is also just Reader
.
It goes on and on. For example, language interpreters also boil down to Reader
, when you apply this model:
- Expression = a
Reader
- Free variables = uses of
ask
- Evaluation environment =
Reader
execution environment. - Binding constructs =
local
A good analogy is that a Reader r a
represents an a
with "holes" in it, that prevent you from knowing which a
we're talking about. You can only get an actual a
once you supply a an r
to fill in the holes. There are tons of things like that. In the examples above, a "history" is a value that can't be computed until you specify a time, a hypercube is a value that can't be computed until you specify an intersection, and a language expression is a value that can't be computed until you supply the values of the variables. It also gives you an intuition on why Reader r a
is the same as r -> a
, because such a function is also intuitively an a
missing an r
.
So the Functor
, Applicative
and Monad
instances of Reader
are a very useful generalization for cases where you are modeling anything of the sort "an a
that's missing an r
," and allow you to treat these "incomplete" objects as if they were complete.
Yet another way of saying the same thing: a Reader r a
is something that consumes r
and produces a
, and the Functor
, Applicative
and Monad
instances are basic patterns for working with Reader
s. Functor
= make a Reader
that modifies the output of another Reader
; Applicative
= connect two Reader
s to the same input and combine their outputs; Monad
= inspect the result of a Reader
and use it to construct another Reader
. The local
and withReader
functions = make a Reader
that modifies the input to another Reader
.
In Java or C++ you may access any variable from anywhere without any problem. Problems appears when your code becomes multi-threaded.
In Haskell you have only two ways to pass the value from one function to another:
- You pass the value through one of input parameters of the callable function. Drawbacks are: 1) you can't pass ALL the variables in that way - list of input parameters just blow your mind. 2) in sequence of function calls:
fn1 -> fn2 -> fn3
, functionfn2
may not need parameter which you pass fromfn1
tofn3
. - You pass the value in scope of some monad. Drawback is: you have to get firm understanding what Monad conception is. Passing the values around is just one of great deal of applications where you may use the Monads. Actually Monad conception is incredible powerful. Don't be upset, if you didn't get insight at once. Just keep trying, and read different tutorials. The knowledge you'll get will pay off.
The Reader monad just pass the data you want to share between functions. Functions may read that data, but can't change it. That's all that do the Reader monad. Well, almost all. There are also number of functions like local
, but for the first time you can stick with asks
only.
참고URL : https://stackoverflow.com/questions/14178889/what-is-the-purpose-of-the-reader-monad
'IT박스' 카테고리의 다른 글
Vim : 비주얼 블록 모드에서 삽입하는 방법? (0) | 2020.07.25 |
---|---|
HTTPS / SSL을 통한 Java 클라이언트 인증서 (0) | 2020.07.25 |
Java .class 버전을 읽고 표시하는 도구 (0) | 2020.07.25 |
이중 백 슬래시가있는 모든 단일 백 슬래시 (0) | 2020.07.25 |
참조 된 어셈블리 PDB 및 XML 파일이 출력으로 복사되지 않도록 방지 (0) | 2020.07.25 |