读者monad的目的是什么?

时间:2013-01-06 03:06:11

标签: haskell monads reader-monad

读者monad是如此复杂,似乎毫无用处。在像Java或C ++这样的命令式语言中,如果我没有弄错的话,读者monad没有相同的概念。

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

3 个答案:

答案 0 :(得分:146)

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

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

  1. monad 做什么 ?它配备了哪些操作?有什么好处?
  2. monad是如何实现的?它从哪里出现?
  3. 从第一种方法来看,读者monad是一种抽象类型

    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。

    读者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, 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!
    

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

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

    就是这样。一个功能齐全的lambda演算解释器。


    另一种思考方式是问:它是如何实现的?答案是读者monad实际上是所有monad中最简单和最优雅的之一。

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

    阅读器只是功能的奇特名称!我们已经定义了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 (f x)) x
    

    这不是那么可怕。 ask非常简单:

    ask = Reader $ \x -> x
    

    虽然local并不是那么糟糕。

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

    好的,所以读者monad只是一个功能。为什么要读者呢?好问题。实际上,你不需要它!

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

    这些甚至更简单。更重要的是,ask只是idlocal只是功能组合,功能的顺序已切换!

答案 1 :(得分:49)

我记得你一直感到困惑,直到我自己发现,阅读器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获取他们所拥有的VP历史记录的函数。

嗯,这个History monad实际上完全Reader相同。 History t aReader t a非常相同(与t -> a相同)。

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

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

对超立方体的一种常见操作是将多位标量函数应用于超立方体的对应点。我们可以通过为Applicative定义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)

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

它一直在继续。例如,当您应用此模型时,语言解释器也会归结为Reader

  • Expression = a Reader
  • 自由变量=使用ask
  • 评估环境= Reader执行环境。
  • 绑定构造= local

一个很好的比喻是,Reader r a代表a,其中包含“洞”,这会阻止您知道我们正在谈论的a。只有在提供a来填补漏洞后,您才能获得实际的r。有很多这样的事情。在上面的示例中,“历史记录”是指定时间之前无法计算的值,超立方体是在指定交集之前无法计算的值,而语言表达式是可以使用的值在提供变量值之前,不要计算。它还为您提供了Reader r ar -> a相同的直觉,因为这样的函数也直观地a缺少r

因此,Functor的{​​{1}},ApplicativeMonad个实例对于您正在对“{{1}”类别进行建模的情况非常有用。 }它缺少Reader,“并允许您将这些”不完整“对象视为完整对象。

另一种说同样话题的方式:a消耗r并生成Reader r ara和{ {1}}实例是使用Functor的基本模式。 Applicative =设置Monad修改另一个Reader的输出; Functor =将两个Reader连接到同一输入并合并其输出; Reader =检查Applicative的结果并使用它构建另一个ReaderMonadReader函数=设置Reader,修改另一个local的输入。

答案 2 :(得分:19)

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

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

  • 您通过可调用函数的输入参数之一传递值。缺点是:1)你不能以这种方式传递所有变量 - 输入参数列表只会让你大吃一惊。 2)按函数调用的顺序:fn1 -> fn2 -> fn3,函数fn2可能不需要从fn1传递到fn3的参数。
  • 您传递了某些monad范围内的值。缺点是:你必须深刻理解Monad的概念。传递价值只是您可以使用Monads的众多应用之一。实际上Monad的构想非常强大。如果你没有立刻获得洞察力,请不要感到沮丧。继续尝试,阅读不同的教程。你将获得的知识将得到回报。

Reader monad只传递您想要在函数之间共享的数据。函数可以读取该数据,但不能更改它。这就是读者monad的全部内容。好吧,几乎所有。还有许多功能,例如local,但这是第一次只能坚持使用asks