读者单子的目的是什么?

读者单子是如此复杂,似乎是无用的。如果我没记错的话,在 Java 或 C + + 这样的命令式语言中,读者单子没有等价的概念。

你能给我举个简单的例子,把这个问题弄清楚一点吗?

40583 次浏览

不要害怕! 阅读器单子实际上并没有那么复杂,而且有真正的易于使用的实用程序。

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

  1. 单子 是什么? 它配备了什么操作? 它有什么好处?
  2. 单子是如何实现的? 它是从哪里产生的?

从第一种方法开始,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。

读者单子也被用来做面向对象的人称为 依赖注入的事情。例如,尼加马克斯算法经常被使用(以高度优化的形式)来计算两人博弈中的一个位置的值。不过算法本身并不关心你在玩什么游戏,除了你需要能够确定游戏中的“下一个”位置,并且你需要能够判断当前位置是否是胜利位置。

 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)

读取器单子只是一个函数。为什么要读者?问得好。事实上,你不需要!

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


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

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

我记得当时和你一样困惑,直到我自己发现 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 m => (a -> m b) -> (b -> m c) -> a -> m c,可以最直观地理解单子实例。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单子实际上是 没错Reader一样的。History t a实际上与 Reader t a(与 t -> a相同)相同。

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

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

我只是复制了上面的 History代码并改了名字。你可以看出来,Hypercube也只是 Reader

例如,当你应用这个模型时,语言解释器也可以归结为 Reader:

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

一个很好的类比是,Reader r a表示一个带有“孔”的 a,这使您无法知道我们正在讨论的是哪个 a。你只能得到一个实际的 a一旦你提供了一个 r填补的洞。这样的事情太多了。在上面的例子中,“历史”是指在指定时间之前无法计算的值,超立方体是指在指定交集之前无法计算的值,语言表达式是指在提供变量值之前无法计算的值。它也给你一个直觉,为什么 Reader r a是相同的 r -> a,因为这样的函数也是一个直观的 a缺少一个 r

因此,ReaderFunctorApplicativeMonad实例对于您正在建模任何类似于“缺少 ra”的情况是非常有用的泛化,并且允许您将这些“不完整”对象视为完整的。

还有另一种说法: Reader r a是消耗 r并产生 a的东西,而 FunctorApplicativeMonad实例是使用 Reader的基本模式。Functor = 制作一个 Reader来修改另一个 Reader的输出; Applicative = 将两个 Reader连接到同一个输入并合并它们的输出; Monad = 检查一个 Reader的结果并用它来构造另一个 Readerr5和 r6函数 = 创建一个 Reader,将输入修改为另一个 Reader

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

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

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

Reader 单子只传递要在函数之间共享的数据。函数可以读取该数据,但不能更改它。这就是读者单元的全部内容。几乎全部。还有很多类似于 local的函数,但是第一次你只能使用 asks