[英]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:我们可以问
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
只是id
, local
只是功能组合,功能顺序切换!
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.即,
Functor
和Applicative
允许我们调整常规的、非历史的函数来处理历史。
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 Supervisor
和getVP :: 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
是获取任何Person
的VP
历史记录的函数。
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
:
Reader
Reader
ask
ask
使用Reader
execution environment.Reader
执行环境。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 ra
与r -> 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.所以
Reader
的Functor
、 Applicative
和Monad
实例是一个非常有用的概括,适用于您正在建模任何类型的“缺少r
的a
”,并允许您将这些“不完整”的对象视为完整的对象。
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
东西,而Functor
、 Applicative
和Monad
实例是使用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
. local
和withReader
函数 = 创建一个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 中,只有两种方法可以将值从一个函数传递到另一个函数:
fn1 -> fn2 -> fn3
, function fn2
may not need parameter which you pass from fn1
to fn3
. fn1 -> fn2 -> fn3
,功能fn2
可能不需要你从传递参数fn1
至fn3
。 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.