简体   繁体   English

读者单子的目的是什么?

[英]What is the purpose of the reader monad?

The reader monad is so complex and seems to be useless. reader monad 太复杂了,似乎没用。 In an imperative language like Java or C++, there is no equivalent concept for the reader monad, if I am not mistaken.如果我没记错的话,在像 Java 或 C++ 这样的命令式语言中,对于 reader monad 没有等效的概念。

Can you give me a simple example and clear this up a little bit?你能给我一个简单的例子并澄清一点吗?

Don't be scared!不要害怕! The reader monad is actually not so complicated, and has real easy-to-use utility. reader monad 实际上并没有那么复杂,并且具有真正易于使用的实用程序。

There are two ways of approaching a monad: we can ask有两种方法可以接近 monad:我们可以问

  1. What does the monad do ? monad 有什么作用 What operations is it equipped with?它配备了哪些操作? What is it good for?到底有什么好处呢?
  2. How is the monad implemented? monad 是如何实现的? From where does it arise?它从哪里产生?

From the first approach, the reader monad is some abstract type从第一种方法来看,reader monad 是某种抽象类型

data Reader env a

such that以至于

-- 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

So how do we use this?那么我们如何使用它呢? Well, the reader monad is good for passing (implicit) configuration information through a computation.好吧,reader monad 非常适合通过计算传递(隐式)配置信息。

Any time you have a "constant" in a computation that you need at various points, but really you would like to be able to perform the same computation with different values, then you should use a reader monad.任何时候您在不同点需要的计算中有一个“常数”,但实际上您希望能够使用不同的值执行相同的计算,那么您应该使用阅读器 monad。

Reader monads are also used to do what the OO people call dependency injection . Reader monads 也被用来做 OO 人所说的依赖注入 For example, the negamax algorithm is used frequently (in highly optimized forms) to compute the value of a position in a two player game.例如, negamax算法经常(以高度优化的形式)用于计算两人游戏中位置的值。 The algorithm itself though does not care what game you are playing, except that you need to be able to determine what the "next" positions are in the game, and you need to be able to tell if the current position is a victory position.不过,算法本身并不关心您正在玩什么游戏,只是您需要能够确定游戏中的“下一个”位置是什么,并且您需要能够判断当前位置是否是胜利位置。

 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

This will then work with any finite, deterministic, two player game.这将适用于任何有限的、确定性的、两人游戏。

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:好吧,一种选择是使用 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 .好的,所以 Haskell 和其他函数式语言都是基于lambda 演算的 Lambda calculus has a syntax that looks like Lambda 演算的语法看起来像

 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, let's write the interpreter:所以,让我们编写解释器:

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!

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.用于 lambda 演算的全功能解释器。


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.答案是 reader monad 实际上是所有 monad 中最简单、最优雅的一种。

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

Reader is just a fancy name for functions! Reader 只是函数的一个奇特名称! We have already defined runReader so what about the other parts of the API?我们已经定义了runReader那么 API 的其他部分呢? Well, every Monad is also a Functor :好吧,每个Monad也是一个Functor

instance Functor (Reader env) where
   fmap f (Reader g) = Reader $ f . g

Now, to get a monad:现在,要获得一个 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真的很简单:

ask = Reader $ \x -> x

while local isn't so bad:local并没有那么糟糕:

local f (Reader g) = Reader $ \x -> runReader g (f x)

Okay, so the reader monad is just a function.好的,所以 reader monad 只是一个函数。 Why have Reader at all?为什么有 Reader? 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!更重要的是, ask只是idlocal只是功能组合,功能顺序切换!

I remember being puzzled as you were, until I discovered on my own that variants of the Reader monad are everywhere .我记得和你一样困惑,直到我自己发现 Reader monad 的变体无处不在 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: 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

Ie, Functor and Applicative allow us to adapt regular, non-historical functions to work with histories.即, FunctorApplicative允许我们调整常规的、非历史的函数来处理历史。

The monad instance is most intuitively understood by considering the function (>=>) :: Monad m => (a -> mb) -> (b -> mc) -> a -> mc .通过考虑函数(>=>) :: Monad m => (a -> mb) -> (b -> mc) -> a -> mc可以最直观地理解 monad 实例。 A function of type a -> History tb is a function that maps an a to a history of b values; a -> History tb类型的函数是a将 a 映射到b值的历史的函数; for example, you could have getSupervisor :: Person -> History Day Supervisor , and getVP :: Supervisor -> History Day VP .例如,您可以使用getSupervisor :: Person -> History Day SupervisorgetVP :: Supervisor -> History Day VP So the Monad instance for History is about composing functions like these;因此, History的 Monad 实例是关于组合这样的函数; 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.例如, getSupervisor >=> getVP :: Person -> History Day VP是获取任何PersonVP历史记录的函数。

Well, this History monad is actually exactly the same as Reader .嗯,这个History monad 实际上Reader完全一样。 History ta is really the same as Reader ta (which is the same as t -> a ). History ta实际上与Reader ta相同(与t -> a相同)。

Another example: I've been prototyping OLAP designs in Haskell recently.另一个例子:我最近一直在 Haskell 中进行OLAP设计的原型设计。 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 :我们可以通过为Hypercube定义一个Applicative实例来获得:

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.我只是复制粘贴上面的History代码并更改了名称。 As you can tell, Hypercube is also just Reader .如您所知, Hypercube也只是Reader

It goes on and on.它会一直持续下去。 For example, language interpreters also boil down to Reader , when you apply this model:例如,当您应用此模型时,语言解释器也归结为Reader

  • Expression = a Reader表达式 = Reader
  • Free variables = uses of ask自由变量 = ask使用
  • Evaluation environment = Reader execution environment.评估环境 = Reader执行环境。
  • Binding constructs = local绑定结构 = local

A good analogy is that a Reader ra represents an a with "holes" in it, that prevent you from knowing which a we're talking about.一个很好的类比是Reader ra代表a带有“洞”的a ,它阻止你知道我们在谈论哪个a You can only get an actual a once you supply a an r to fill in the holes.一旦您提供 a an r来填充漏洞,您只能获得实际a 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 ra is the same as r -> a , because such a function is also intuitively an a missing an r .它还让您直观了解为什么Reader rar -> a相同,因为这样的函数在直觉上也是a缺少 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.所以ReaderFunctorApplicativeMonad实例是一个非常有用的概括,适用于您正在建模任何类型的“缺少ra ”,并允许您将这些“不完整”的对象视为完整的对象。

Yet another way of saying the same thing: a Reader ra is something that consumes r and produces a , and the Functor , Applicative and Monad instances are basic patterns for working with Reader s.另一种说法是: Reader ra是消耗r并产生a东西,而FunctorApplicativeMonad实例是使用Reader的基本模式。 Functor = make a Reader that modifies the output of another Reader ; Functor = 制作一个Reader来修改另一个Reader的输出; Applicative = connect two Reader s to the same input and combine their outputs; Applicative = 将两个Reader连接到相同的输入并组合它们的输出; Monad = inspect the result of a Reader and use it to construct another Reader . Monad = 检查Reader的结果并使用它来构造另一个Reader The local and withReader functions = make a Reader that modifies the input to another Reader . localwithReader函数 = 创建一个Reader来修改另一个Reader的输入。

In Java or C++ you may access any variable from anywhere without any problem.在 Java 或 C++ 中,您可以从任何地方访问任何变量而不会出现任何问题。 Problems appears when your code becomes multi-threaded.当您的代码变为多线程时会出现问题。

In Haskell you have only two ways to pass the value from one function to another:在 Haskell 中,只有两种方法可以将值从一个函数传递到另一个函数:

  • 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.缺点是:1)您不能以这种方式传递所有变量 - 输入参数列表只会让您大吃一惊。 2) in sequence of function calls: fn1 -> fn2 -> fn3 , function fn2 may not need parameter which you pass from fn1 to fn3 . 2)在函数调用序列: fn1 -> fn2 -> fn3 ,功能fn2可能不需要你从传递参数fn1fn3
  • You pass the value in scope of some monad.您在某个 monad 的范围内传递值。 Drawback is: you have to get firm understanding what Monad conception is.缺点是:你必须深入了解 Monad 的概念。 Passing the values around is just one of great deal of applications where you may use the Monads.传递值只是您可以使用 Monad 的大量应用程序之一。 Actually Monad conception is incredible powerful.实际上 Monad 的概念是非常强大的。 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. Reader monad 只是传递您想要在函数之间共享的数据。 Functions may read that data, but can't change it.函数可以读取该数据,但不能更改它。 That's all that do the Reader monad.这就是 Reader monad 的全部功能。 Well, almost all.嗯,几乎所有。 There are also number of functions like local , but for the first time you can stick with asks only.还有许多函数,如local ,但第一次你只能坚持使用asks

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM