2013-01-06 49 views
89

阅读器Monad非常复杂,似乎没用。在像Java或C++这样的命令式语言中,对于读者monad来说没有等价的术语(如果我是对的)。阅读器Monad的目的是什么?

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

+19

如果您想 - 有时从(不可修改的)环境读取一些值,但不希望显式传递该环境,则可以使用读者monad。在Java或C++中,您可以使用全局变量(虽然它不完全相同)。 –

+5

@丹尼尔:听起来非常像一个*答案* – SingleNegationElimination

+0

@TokenMacGuy答案太短了,现在想想更长的时间已经太晚了。如果没有其他人做,我会睡觉后。 –

回答

117

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

有接近一个单子的方式有两种:我们可以问

  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 

那么,我们如何使用它?那么,读者monad就可以通过计算来传递(隐含的)配置信息。当你需要在不同的点进行计算时,如果你有一个“常量”,但是你真的希望能够用不同的值执行相同的计算,那么你应该使用一个阅读器monad。

读者单子也被用来做什么OO人称之为dependency injection。例如,negamax算法经常使用(以高度优化的形式)以计算双人游戏中的位置值。该算法本身虽然不关心你在玩什么游戏,但你必须能够确定什么是“下一个”位置是在游戏中,你需要能够告诉如果当前位置是一个胜利的姿势。

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 

那么这将与任何有限的,确定的,两个玩家的游戏工作。

这种模式即使事情是不是真的依赖注入是非常有用的。假设你在财务部门工作,你可能会设计一些复杂的定价资产的逻辑(衍生品说),这是一切都很好,你可以做到没有任何发臭的单子。但是,你修改程序来处理多种货币。您需要能够在不同货币之间进行转换。你的第一次尝试是定义一个顶级功能

type CurrencyDict = Map CurrencyName Dollars 
currencyDict :: CurrencyDict 

拿到现货价格。然后你可以在你的代码中调用这个字典....但是等等!那不行!货币字典是不可变的,因此不仅对于程序的生命是一样的,而且从它得到的时间编译为!所以你会怎么做?那么一种选择是使用Reader monad:

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

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

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

好吧,Haskell和其他功能的语言都是基于lambda calculus。 Lambda微积分的语法看起来像

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

并且我们要为此语言编写评估程序。为此,我们需要跟踪一个环境,这是一个与术语相关的绑定列表(实际上,因为我们想要进行静态范围设定,所以它会关闭)。

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


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

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

阅读器只是一个功能的花哨名称!我们已经定义了runReader那么API的其他部分呢?那么每一个Monad也是Functor

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

现在,得到一个单子:

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

这是不那么吓人。 ask是非常简单的:

ask = Reader $ \x -> x 

local并没有那么糟糕。

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

好了,所以读者单子只是一个功能。为什么有读者?好问题。其实,你不需要它!

instance Functor ((->) env) where 
    fmap = (.) 

instance Monad ((->) env) where 
    return = const 
    f >>= g = \x -> g (f x) x 

这是更简单。更何况,ask只是idlocal只是在其他顺序的功能组成!

+4

非常有趣的答案。说实话,我多次阅读它,当我想审查monad。顺便说一下,关于nagamax算法,“values < - mapM(negate。我知道,你提供的代码只是为了展示读者单子如何工作,但是如果你有时间,你能纠正negamax算法的代码吗?因为有趣的是,当你使用reader monad来解决negamax – chipbk10

+4

所以'Reader'是一个具有monad类型类的特定实现的函数吗?以前说过会让我感到困惑一点,首先我没有得到它。我认为:“哦,它允许你返回一些能够在你提供缺失值时给你想要的结果的东西。”我认为这很有用,但是突然意识到一个函数完全可以做到这一点 – ziggystar

+1

在阅读了这篇文章之后,我了解了大部分内容。'local'函数确实需要更多的解释.. –

38

我记得当时很困惑,直到我自己发现读卡器monad的变种都是无处不在。我是如何发现它的?因为我一直在写代码,结果是它的小变化。

例如,我曾经写过一些代码来处理历史的值;随时间变化的值。一个非常简单的这种模式是在该点的时间点函数值的时间:

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 

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允许我们适应规律的,非历史的功能来处理历史。

通过考虑函数(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c可以最直观地理解monad实例。类型为a -> History t b的函数是将a映射到b值的历史的函数;例如,您可以有getSupervisor :: Person -> History Day SupervisorgetVP :: Supervisor -> History Day VP。因此,History的Monad实例是关于组合这些功能的;例如,getSupervisor >=> getVP :: Person -> History Day VP是获取(Person)所具有的历史记录的功能。

那么,这个History monad其实就是,就是,就跟Reader一样。 History t aReader t a(与t -> a相同)确实相同。

另一个例子:我最近在Haskell设计了原型OLAP。这里的一个想法是“超立方体”,它是从一组维度的交集到值的映射。在这里,我们又来了:

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 @[email protected] hypercube to its corresponding point 
    -- in @[email protected] 
    ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x) 

我只是copypasted以上History代码改了个名字。如您所知,Hypercube也只是Reader

它继续下去。例如,语翻译也归结为Reader,在应用此模型:

  • 表达式= ask
  • 评价环境= Reader执行环境的Reader
  • 自由变量=用途。
  • 结合构建= local

一个很好的比喻是,Reader r a代表与它“洞”,即防止你知道哪些a我们谈论的a。一旦你提供一个r来填补洞,你只能得到一个实际的a。有很多这样的事情。在上面的例子中,“历史”是一个值,只有指定时间后才能计算,超立方体是指定交集之前无法计算的值,而语言表达式是可以计算的值直到你提供变量的值才被计算。它也给你一个直觉,因为Reader r a为什么与r -> a相同,因为这样的功能也是直观的a缺少r

所以FunctorApplicativeReaderMonad情况是因为你所排序的造型任何情况下,一个非常有用的泛化“的a是不可少的r”,并允许您将这些“不完整”的对象,就好像他们完成了。

说同样的事情的另一种方法:一个Reader r a是一件消耗r并产生aFunctorApplicativeMonad情况下是基本模式与Reader s工作。 Functor =使Reader修改另一个Reader的输出; Applicative =将两个Reader连接到相同的输入并组合它们的输出; Monad =检查Reader的结果,并用它来构造另一个ReaderlocalwithReader函数=使Reader将输入修改为另一个Reader

+4

很棒的回答。您还可以使用'GeneralizedNewtypeDeriving'扩展名来根据基础类型为newty派生'Functor','Applicative','Monad'等。 –

13

在Java或C++中,您可以从任何位置访问任何变量,而不会有任何问题。您的代码变为多线程时出现问题。

在Haskell你只有两种方式的值传递从一个功能到另一个:

  • 您通过的可调用函数的输入参数一个传递价值。缺点是:1)你不能以这种方式传递所有的变量 - 输入参数列表只是让你头脑发热。 2)按函数调用顺序:fn1 -> fn2 -> fn3,函数fn2可能不需要您从fn1fn3传递的参数。
  • 你传递一些monad的值。缺点是:你必须牢固理解Monad的概念是什么。将值传递给您可能会使用Monads的应用程序只是其中的一个。其实Monad的构想是非常强大的。如果你一次没有得到洞察力,不要感到不安。只要继续尝试,并阅读不同的教程。你将获得的知识将会得到回报。

Reader monad只是传递你想在功能之间共享的数据。函数可能会读取该数据,但不能更改它。这就是Reader monad的全部功能。好吧,几乎所有。还有一些功能,如local,但第一次只能坚持asks

+2

使用monads隐式传递数据的另一个缺点是,很容易发现自己在'do'-notation中编写了大量的'命令式'代码,这会更好地被重构为纯函数。 –

+3

@BenjaminHodgson在do -notation中写入'单向性'代码并不意味着编写副作用(不纯)代码。实际上,Haskell中的副作用代码可能只能在IO monad中使用。 –