What is the purpose of the reader monad?

前端 未结 3 1825
暖寄归人
暖寄归人 2020-11-29 15:37

The reader monad is so complex and seems to be useless. In an imperative language like Java or C++, there is no equivalent concept for the reader monad, if I am not mistaken

3条回答
  •  囚心锁ツ
    2020-11-29 15:50

    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.

提交回复
热议问题