读者monad的目的是什么?

chi*_*k10 115 monads haskell reader-monad

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

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

Phi*_* JF 157

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

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

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

从第一种方法来看,读者monad是一种抽象类型

data Reader env a
Run Code Online (Sandbox Code Playgroud)

这样的

-- 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
Run Code Online (Sandbox Code Playgroud)

那么我们如何使用它呢?好吧,读者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
Run Code Online (Sandbox Code Playgroud)

这将适用于任何有限的,确定性的,双人游戏.

即使对于非依赖注入的事物,此模式也很有用.假设你在金融领域工作,你可能会设计一些复杂的逻辑来定价资产(一个衍生的说法),这一切都很好,你可以做到没有任何臭的monad.但是,您修改程序以处理多种货币.您需要能够即时转换货币.您的第一次尝试是定义顶级功能

type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
Run Code Online (Sandbox Code Playgroud)

获得现货价格.然后你可以在你的代码中调用这个字典....但是等等!那不行!货币字典是不可变的,因此不仅必须与程序的生命周期相同,而且必须从编译时开始!所以你会怎么做?好吧,一个选择是使用Reader monad:

 computePrice :: Reader CurrencyDict Dollars
 computePrice
    = do currencyDict <- ask
      --insert computation here
Run Code Online (Sandbox Code Playgroud)

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

 local :: (env -> env) -> Reader env a -> Reader env a
Run Code Online (Sandbox Code Playgroud)

好的,所以Haskell和其他函数式语言都是基于lambda演算.Lambda演算的语法看起来像

 data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
Run Code Online (Sandbox Code Playgroud)

我们想为这种语言写一个评估者.为此,我们需要跟踪一个环境,这是一个与术语相关的绑定列表(实际上它将是闭包,因为我们想要进行静态作用域).

 newtype Env = Env ([(String,Closure)])
 type Closure = (Term, Env)
Run Code Online (Sandbox Code Playgroud)

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

 data Value = Lam String Closure | Failure String
Run Code Online (Sandbox Code Playgroud)

所以,让我们写一下翻译:

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!
Run Code Online (Sandbox Code Playgroud)

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

interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
Run Code Online (Sandbox Code Playgroud)

就是这样.lambda演算的全功能解释器.


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

newtype Reader env a = Reader {runReader :: env -> a}
Run Code Online (Sandbox Code Playgroud)

读者只是功能的一个奇特名称!我们已经定义runReader了API的其他部分呢?嗯,每个人Monad也是Functor:

instance Functor (Reader env) where
   fmap f (Reader g) = Reader $ f . g
Run Code Online (Sandbox Code Playgroud)

现在,要获得一个monad:

instance Monad (Reader env) where
   return x = Reader (\_ -> x)
   (Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
Run Code Online (Sandbox Code Playgroud)

这不是那么可怕. ask很简单:

ask = Reader $ \x -> x
Run Code Online (Sandbox Code Playgroud)

虽然local不是那么糟糕.

local f (Reader g) = Reader $ \x -> runReader g (f x)
Run Code Online (Sandbox Code Playgroud)

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

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

instance Monad ((->) env) where
  return = const
  f >>= g = \x -> g (f x) x
Run Code Online (Sandbox Code Playgroud)

这些甚至更简单.更重要的是,ask仅仅是idlocal只是函数组合与交换功能的命令!

  • 非常有趣的答案.老实说,当我想复习monad时,我再次读了很多次.顺便说一下,关于nagamax算法,"值< - mapM(否定.negamax(否定颜色))可能"似乎不正确.我知道,你提供的代码只是为了展示读者monad的工作原理.但如果你有时间,你能纠正negamax算法的代码吗?因为,当你使用读者monad解决negamax时,这很有趣. (5认同)
  • 所以`Reader`是一个具有monad类型的特定实现的函数?早点说它会让我感到困惑.首先,我没有得到它.一半我想"哦,它允许你返回一些东西,一旦你提供了缺失值,将给你想要的结果." 我认为这很有用,但突然意识到函数正是这样做的. (4认同)

Lui*_*las 52

我记得你一直很困惑,直到我自己发现读者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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

即,FunctorApplicative让我们适应的正常,非历史作用与历史的工作.

通过考虑函数,可以最直观地理解monad实例(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c.类型a -> History t b的函数是将值映射ab值的历史的函数; 例如,你可以拥有getSupervisor :: Person -> History Day Supervisor,和getVP :: Supervisor -> History Day VP.所以Monad实例History是关于组合这些函数的; 例如,getSupervisor >=> getVP :: Person -> History Day VP对于任何人来说Person,VP是他们拥有的历史.

嗯,这个Historymonad实际上完全一样Reader. History t a是真的一样Reader t a(与之相同t -> a).

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

newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
Run Code Online (Sandbox Code Playgroud)

对超立方体的一种常见操作是将多位标量函数应用于超立方体的对应点.我们可以通过定义一个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)
Run Code Online (Sandbox Code Playgroud)

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

它会一直持续下去.例如,Reader当您应用此模型时,语言解释器也可归结为:

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

一个很好的类比是a Reader r a代表一个a带有"洞"的东西,这会阻止你知道a我们在说什么.a一旦你提供一个r填充孔,你就只能得到一个实际的.有很多这样的事情.在上面的示例中,"历史记录"是指定时间之前无法计算的值,超立方体是在指定交集之前无法计算的值,而语言表达式是可以使用的值在提供变量值之前,不要计算.它还为你提供了一个直觉,为什么Reader r a是相同的r -> a,因为这样的功能也直观地a缺少了r.

因此Functor,对于您正在对" 缺少一个" 的类型进行建模的情况,这些ApplicativeMonad实例Reader是非常有用的概括,并允许您将这些"不完整"对象视为完整对象.ar

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

  • 很好的答案.您还可以使用`GeneralizedNewtypeDeriving`扩展来基于其基础类型为newtypes派生`Functor`,`Applicative`,`Monad`等. (5认同)

Dmi*_*lov 19

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

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

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

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

  • @BenjaminHodgson在do -notation中用monad编写'命令式'代码并不一定意味着编写副作用(不纯)代码.实际上,Haskell中的副作用代码可能只能在IO monad中使用. (4认同)
  • 使用monad隐式传递数据的另一个缺点是,很容易发现自己在`do`-notation中编写了许多"命令式"代码,最好将其重构为纯函数. (3认同)