Reader Monad的目的是什么?

读者单子是如此复杂,似乎是无用的。 在像Java或C ++这样的命令式语言中,对于读者monad来说,没有相同的术语(如果我是对的)。

你能给我一个简单的例子,让我清楚一点吗?

不要害怕! 阅读器monad实际上并不复杂,并且具有真正易用的实用程序。

接近单子有两种方法:我们可以问

  1. monad是什么的? 它配备了什么操作? 到底有什么好处呢?
  2. monad是如何实现的? 它从哪里出现?

从第一种方法来看,读者monad是一些抽象types

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 

那么我们如何使用这个? 那么,读者monad是好的通过计算传递(隐式)的configuration信息。

任何时候你在计算中都需要一个“常数”,但是你真的希望能够用不同的值执行相同的计算,那么你应该使用一个读者monad。

读者单子也被用来做OO的人所谓的dependency injection 。 例如, negamaxalgorithm经常使用(以高度优化的forms)来计算双人游戏中的位置值。 algorithm本身并不关心你在玩什么游戏,除了你需要能够确定游戏中“下一个”的位置是什么,你需要能够判断当前的位置是否是一个胜利的位置。

  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 

然后,这将与任何有限的,确定性的双人游戏一起工作。

即使对于那些不是真正的dependency injection的东西,这种模式也是有用的。 假设你在金融工作,你可能会devise一些复杂的定价资产的逻辑(衍生物说),这是一切顺利,你可以做到没有任何臭monad。 但是,那么,你修改你的程序来处理多种货币。 您需要能够在不同货币之间进行转换。 你的第一个尝试是定义一个顶层函数

 type CurrencyDict = Map CurrencyName Dollars currencyDict :: CurrencyDict 

获得现货价格。 你可以在代码中调用这个字典….但是等等! 那不行! 货币字典是不可变的,所以不仅对于你的程序的生命是一样的,而且从编译的时候开始 ! 所以你会怎么做? 那么一个select是使用Reader monad:

  computePrice :: Reader CurrencyDict Dollars computePrice = do currencyDict <- ask --insert computation here 

也许最经典的用例是实现解释器。 但是,在我们看之前,我们需要引入另一个function

  local :: (env -> env) -> Reader env a -> Reader env a 

好的,Haskell和其他函数式语言都是基于lambda演算的 。 Lambda微积分的语法如下所示

  data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show) 

我们想写一个这种语言的评估者。 为此,我们需要跟踪一个环境,这是一个与条款相关联的绑定列表(实际上,因为我们想要进行静态范围设定,所以会closures)。

  newtype Env = Env ([(String,Closure)]) type Closure = (Term,Env) 

当我们完成后,我们应该得出一个值(或一个错误):

  data Value = Lam String Closure | Failure String 

那么,让我们来翻译一下:

 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, than 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! 

最后,我们可以通过传递一个微不足道的环境来使用它:

 interp :: Term -> Value interp term = runReader (interp' term) (Env []) 

就是这样。 lambda演算的全function解释器。


所以,另一种思考这个问题的方法是问:它是如何实现的? 那么答案是,读者monad实际上是所有单子中最简单最优雅的一个。

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

读者只是function的一个奇特的名字! 我们已经定义了runReader那么API的其他部分呢? 那么每个Monad也是一个Functor

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

现在,要获得monad:

 instance Monad (Reader env) where return x = Reader (\_ -> x) (Reader f) >>= g = Reader $ \x -> runReader (g (fx)) x 

这并不那么可怕。 ask很简单:

 ask = Reader $ \x -> x 

local并不是那么糟糕。

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

好的,所以读者的monad只是一个function。 为什么有读者呢? 好问题。 其实,你不需要它!

 instance Functor ((->) env) where fmap = (.) instance Monad ((->) env) where return = const f >>= g = \x -> g (fx) x 

这些更简单。 更重要的是,只要idlocal是其他顺序的函数组合就可以了!

我记得当时你很困惑,直到我自己发现读者monad的变体无处不在 。 我怎么发现它? 因为我一直在写代码,结果是它的小变化。

例如,我曾经写过一些代码来处理历史价值; 随时间变化的值。 一个非常简单的模型是从时间点到这个时间点的值:

 import Control.Applicative -- | A History with timeline type t and value type a. newtype History ta = 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 

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 

也就是说, FunctorApplicative允许我们适应规律的,非历史的function来处理历史。

通过考虑函数(>=>) :: Monad m => (a -> mb) -> (b -> mc) -> a -> mc monad (>=>) :: Monad m => (a -> mb) -> (b -> mc) -> a -> mc可以最直观地理解monad实例。 typesa -> History tb的函数是将a映射到b值历史的函数; 例如,你可以有getSupervisor :: Person -> History Day SupervisorgetVP :: Supervisor -> History Day VP 。 因此, History的Monad实例是关于如何编写这样的函数的; 例如, getSupervisor >=> getVP :: Person -> History Day VP是获取任何PersonVP s历史的函数。

那么,这个History monad实际上 Reader 完全一样。 History taReader ta是一样的(与t -> a相同)。

另一个例子:我最近在Haskell中进行了原型devise。 这里的一个想法是“超立方体”,它是从一组维度的交集到值的映射。 再来一次:

 newtype Hypercube intersection value = Hypercube { get :: intersection -> value } 

在超立方体上操作的一个共同点是将多位标量函数应用于超立方体的对应点。 我们可以通过为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) 

我刚刚复制上面的History代码,并更改名称。 如你所知, Hypercube也只是Reader

它会一直持续下去。 例如,语言翻译也归结为Reader ,当你应用这个模型:

  • expression式= Reader
  • 自由variables=使用ask
  • 评估环境= Reader执行环境。
  • 绑定结构= local

一个很好的比喻是, Reader ra代表a有“漏洞”的a ,它阻止你知道我们在说什么。 一旦你提供了一个填充洞,你只能得到一个实际的a 。 有很多这样的事情。 在上面的例子中,“历史”是指定一个时间后才能计算的值,超直angular是指定交点之前无法计算的值,而语言expression式是一个值直到你提供variables的值才被计算。 它也给了你一个为什么Reader rar -> a相同的直觉,因为这样的函数在直觉上也是a缺失的r

因此, ReaderFunctorApplicativeMonad实例是一个非常有用的概括,用于对“缺lessra类”进行build模的任何情况,并允许您将这些“不完整”的对象看作是完整的。

还有另一种说法: Reader ra是消耗r和产生a东西,而FunctorApplicativeMonad实例是与Reader s一起工作的基本模式。 Functor =使Reader修改另一个Reader的输出; Applicative =将两个Reader连接到相同的input并组合它们的输出; Monad =检查Reader的结果并用它来构造另一个Readerlocal和与withReaderfunction=使Reader修改input到另一个Reader

在Java或C ++中,您可以从任何地方访问任何variables,而不会有任何问题。 当代码变成multithreading时出现问题。

在Haskell中,只有两种方法将值从一个函数传递到另一个函数:

  • 您可以通过可调用函数的input参数之一传递该值。 缺点是:1)你不能以这种方式传递所有的variables – input参数列表只是让你头脑发热。 2)函数调用顺序: fn1 -> fn2 -> fn3 ,函数fn2可能不需要从fn1fn3传递的参数。
  • 你传递一些monad的范围的值。 缺点是:你必须牢固理解Monad的概念是什么。 将值传递给你可以使用Monad的应用程序只是其中的一个。 其实Monad的构想是令人难以置信的强大。 如果你一下子没有了解,不要心烦意乱。 只要继续尝试,并阅读不同的教程。 你将得到的知识将会得到回报。

Reader monad只是传递你想在function之间共享的数据。 函数可能会读取这些数据,但不能更改它。 这就是读者monad所做的一切。 好吧,几乎所有的。 还有一些像localfunction,但是第一次只能坚持asks