别害怕! reader monad 实际上并没有那么复杂,并且具有真正易于使用的实用性。
有两种处理单子的方法:我们可以问
- 单子有什么作用do?它配备了哪些操作?到底有什么好处呢?
- 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。
Reader 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
以获得现货价格。然后你可以在代码中调用这个字典......但是等等!那是不行的!货币字典是不可变的,因此不仅在程序的生命周期内必须相同,而且从它获得的时间起也必须相同compiled!所以你会怎么做?好吧,一种选择是使用 Reader monad:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
也许最经典的用例是实现解释器。但是,在我们看这个之前,我们需要介绍另一个函数
local :: (env -> env) -> Reader env a -> Reader env a
好的,Haskell 和其他函数式语言都是基于拉姆达演算。 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 的其他部分呢?嗯,每一个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
while 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
这些就更简单了。更重要的是,ask
只是id
and local
只是函数组合,并且函数的顺序交换了!