IT박스

독자 모나드의 목적은 무엇입니까?

itboxs 2020. 7. 25. 10:44
반응형

독자 모나드의 목적은 무엇입니까?


독자 모나드는 너무 복잡해서 쓸모없는 것 같습니다. Java 또는 C ++과 같은 명령형 언어에서는 내가 실수하지 않으면 독자 모나드에 대한 동등한 개념이 없습니다.

간단한 예를 들어 주시고 약간 정리해 주실 수 있습니까?


겁내지 마! 리더 모나드는 실제로 그렇게 복잡하지 않으며 사용하기 쉬운 유틸리티를 가지고 있습니다.

모나드에 접근하는 방법에는 두 가지가 있습니다.

  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 사람들이 의존성 주입 이라고 부르는 것을하기 위해 사용된다 . 예를 들어, 네가 막스 알고리즘은 두 플레이어 게임에서 위치 값을 계산하기 위해 자주 (고도로 최적화 된 형태로) 사용됩니다. 알고리즘 자체는 게임에서 "다음"위치를 결정할 수 있어야하고 현재 위치가 승리 위치인지 알 수 있어야한다는 점을 제외하고는 어떤 게임을하든 상관하지 않습니다.

 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 VPs 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 Readers. Functor = make a Reader that modifies the output of another Reader; Applicative = connect two Readers 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, function fn2 may not need parameter which you pass from fn1 to fn3.
  • 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

반응형