141

阅读器单子是如此复杂,似乎无用。如果我没记错的话,在像 Java 或 C++ 这样的命令式语言中,对于 reader monad 没有等效的概念。

你能给我一个简单的例子并澄清一下吗?

4

3 回答 3

198

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

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

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

从第一种方法来看,reader 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

那么我们如何使用它呢?好吧,reader monad 非常适合通过计算传递(隐式)配置信息。

任何时候你在计算中都有一个在不同点需要的“常数”,但实际上你希望能够使用不同的值执行相同的计算,那么你应该使用 reader monad。

Reader monads 也被用来做 OO 人所说的依赖注入。例如,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 演算的。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 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!

最后,我们可以通过传递一个简单的环境来使用它:

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

就是这样。用于 lambda 演算的全功能解释器。


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

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

Reader 只是函数的一个花哨的名称!我们已经定义runReader了 API 的其他部分呢?好吧,everyMonad也是一个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)

好的,所以 reader monad 只是一个函数。为什么要有Reader?好问题。其实你不需要!

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

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

这些甚至更简单。更重要的askidlocal只是功能组合与功能的顺序切换!

于 2013-01-06T05:52:39.087 回答
63

我记得和你一样困惑,直到我自己发现 Reader 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

即,Functor允许Applicative我们调整常规的、非历史的函数来处理历史。

考虑到函数,最直观地理解 monad 实例(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c。类型a -> History t b函数是将 an 映射a到值历史的b函数;例如,您可以拥有getSupervisor :: Person -> History Day SupervisorgetVP :: Supervisor -> History Day VP。所以 Monad 的实例History是关于组合这样的函数;例如,getSupervisor >=> getVP :: Person -> History Day VP对于 any 来说,是获取他们所拥有的 sPerson历史的函数。VP

好吧,这个monadHistory实际上与. 确实与(与 相同)相同。ReaderHistory t aReader t at -> 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例如,当您应用此模型时,语言解释器也归结为:

  • 表达式 = aReader
  • 自由变量 = 使用ask
  • 评估环境=Reader执行环境。
  • 绑定结构 =local

一个很好的类比是 aReader r a代表一个a带有“洞”的“洞”,这会让你不知道a我们在说什么。a一旦你提供一个r填补漏洞,你只能得到一个实际的。有很多这样的事情。在上面的示例中,“历史”是在指定时间之前无法计算的值,超立方体是在指定交集之前无法计算的值,语言表达式是可以'在您提供变量值之前不会被计算。它还让您直观地了解为什么Reader r a与 相同r -> a,因为这样的功能在直觉上也是a缺少的r

因此Functor,ApplicativeMonad的 实例Reader是非常有用的概括,适用于您正在建模任何“a缺少”的东西r,并允许您将这些“不完整”对象视为完整的对象。

还有另一种说法:aReader r a是消费r和生产的东西a,而Functor,ApplicativeMonad实例是使用Readers 的基本模式。 Functor= make aReader修改另一个的输出ReaderApplicative= 将两个Readers 连接到同一个输入并组合它们的输出;Monad= 检查 a 的结果Reader并用它来构造另一个Reader. localandwithReader函数 = make a将Reader输入修改为 another Reader

于 2013-01-08T01:00:45.887 回答
25

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

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

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

Reader monad 只是传递您想要在函数之间共享的数据。函数可以读取该数据,但不能更改它。这就是 Reader monad 所做的一切。嗯,几乎所有。还有很多类似的功能local,但你第一次只能坚持asks

于 2013-01-08T21:56:09.737 回答