读者monad的目的是什么?


122

读者monad是如此复杂,似乎毫无用处。如果我没有记错的话,在命令式语言(如Java或C ++)中,读者monad没有等效的概念。

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


21
如果需要(有时)从(不可修改的)环境中读取一些值,但又不想显式传递该环境,则可以使用阅读器monad。在Java或C ++中,您将使用全局变量(尽管并不完全相同)。
Daniel Fischer 2013年

5
@Daniel:这听起来像是一个可怕的答案
SingleNegationElimination13年

@TokenMacGuy答案太短了,现在考虑更长的时间为时已晚。如果没有其他人睡觉,我会在以后睡觉的。
丹尼尔·菲舍尔

8
在Java或C ++中,Reader monad类似于传递到其构造函数中的对象的配置参数,这些参数在对象的生存期内不会更改。在Clojure中,它有点像动态范围的变量,该变量用于参数化函数的行为,而无需将其显式传递为参数。
danidiaz 2013年

Answers:


169

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

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

  1. 单子什么?它配备什么操作?到底有什么好处呢?
  2. monad如何实现?它从何而来?

从第一种方法看,读者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人所说的依赖注入。例如,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演算的全功能解释器。


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

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

Reader只是功能的花哨名称!我们已经定义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)

好的,所以读者monad只是一个函数。为什么要有读者?好问题。实际上,您不需要它!

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

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

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


6
非常有趣的答案。老实说,当我想回顾monad时,我又读了很多遍。顺便说一下,关于nagamax算法,“值<-mapM(取反。negamax(取反颜色))可能”似乎是不正确的。我知道,您提供的代码仅用于显示阅读器monad的工作方式。但是,如果您有时间,可以更正negamax算法的代码吗?因为,当您使用阅读器monad解决negamax时,这很有趣。
chipbk10

4
那么Reader具有monad类型类的某些特定实现的函数吗?早点说出来会使我感到困惑。首先,我没有得到它。我想过一半,“哦,一旦您提供缺失的值,它就可以返回将给您所需结果的东西。” 我认为这很有用,但突然意识到一个函数可以做到这一点。
ziggystar

1
阅读本文后,我了解了大部分内容。该local功能确实需要尽管一些更多的解释..
克里斯托夫德特洛耶

@Philip我对Monad实例有疑问。我们不能将bind函数写为(Reader f) >>= g = (g (f x))吗?
zeronone

@zeronone在哪里x
Ashish Negi

56

我记得自己像以前一样感到困惑,直到我自己发现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

即,FunctorApplicative允许我们调整常规的,非历史性的功能以处理历史。

通过考虑功能,可以最直观地理解monad实例(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c。类型a -> History t b函数是将an映射ab值的历史的函数;例如,您可能有getSupervisor :: Person -> History Day SupervisorgetVP :: Supervisor -> History Day VP。所以Monad实例History是关于组成这样的函数的;例如,getSupervisor >=> getVP :: Person -> History Day VP该函数可以获取任何人Person拥有的VPs 的历史记录。

那么,这个History单子实际上是完全相同一样ReaderHistory t a确实与Reader t a(与相同t -> a)相同。

另一个示例:我一直在制作OLAP原型最近在Haskell中制作了设计。这里的一个想法是“超立方体”,它是一组维的交集到值的映射。再来一次:

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当您应用此模型时,语言解释器也可以归结为:

  • 表达式= a Reader
  • 免费变量=的使用 ask
  • 评估环境= Reader执行环境。
  • 绑定构造= local

一个很好的类比是,a Reader r a代表一个a带有“孔”的a,从而使您无法知道a我们在说什么。您只需a提供一个r即可填充孔的孔,即可获得实际值。有很多这样的事情。在上面的示例中,“历史记录”是在您指定时间之前无法计算的值,超立方体是在指定交叉点之前无法计算的值,而语言表达式是可以在您提供变量值之前,不进行计算。它还使您直观地了解为什么Reader r a与相同r -> a,因为从直觉上说,这样的功能也是a缺少的r

因此FunctorApplicative和的Monad实例Reader对于在对“a缺少an r” 的情况,并允许您将这些“不完整”对象视为完整对象。

然而,说同样的事情的另一种方式:一个Reader r a是一些消耗r和生产a,以及FunctorApplicativeMonad实例是与工作基本模式Reader秒。 Functor使Reader修改另一个输出Reader; Applicative=将两个Readers 连接到相同的输入并合并它们的输出;Monad=检查a的结果Reader并用它构造另一个Reader。的localwithReader功能=做出Reader该修改输入到另一个Reader


5
好答案。您也可以使用GeneralizedNewtypeDeriving扩展推导FunctorApplicativeMonad,等基于它们的基础类型newtypes。
Rein Henrichs 2014年

20

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

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

  • 您通过可调用函数的输入参数之一传递值。缺点是:1)您不能以这种方式传递所有变量-输入参数列表令人震惊。2)按函数调用顺序:fn1 -> fn2 -> fn3,函数fn2可能不需要您从传递fn1给的参数fn3
  • 您在某个monad范围内传递值。缺点是:您必须完全了解Monad的概念。传递值只是您可以使用Monad的众多应用程序之一。实际上,Monad的概念非常强大。如果您没有一次获得洞察力,请不要难过。继续尝试,然后阅读不同的教程。您将获得的知识将有所回报。

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


3
使用monad隐式传递数据的另一个缺点是,很容易发现自己用do-notation 编写了许多“命令式”代码,最好将其重构为纯函数。
本杰明·霍奇森

4
@BenjaminHodgson用monad的do-符号编写“看起来很权威”的代码并不一定意味着编写副作用(不纯净)的代码。实际上,Haskell中的副作用代码仅可能在IO monad内部。
德米特里·贝斯帕洛夫

如果将另一个函数通过where子句附加到一个函数,是否会将其作为传递变量的第三种方式?
Elmex80s
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.