读者monad是如此复杂,似乎毫无用处。在像Java或C ++这样的命令式语言中,如果我没有弄错的话,读者monad没有相同的概念。
你能给我一个简单的例子并清楚一点吗?
答案 0 :(得分:146)
不要害怕!读者monad实际上并不那么复杂,并且具有真正易用的实用程序。
接近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人所称的dependency injection。例如,频繁使用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 calculus。 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 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演算解释器。
另一种思考方式是问:它是如何实现的?答案是读者monad实际上是所有monad中最简单和最优雅的之一。
newtype Reader env a = Reader {runReader :: env -> a}
阅读器只是功能的奇特名称!我们已经定义了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
虽然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
只是id
而local
只是功能组合,功能的顺序已切换!
答案 1 :(得分:49)
我记得你一直感到困惑,直到我自己发现,阅读器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 m => (a -> m b) -> (b -> m c) -> a -> m c
,可以最直观地理解monad实例。类型a -> History t b
的函数是将a
映射到b
值的历史记录的函数;例如,您可以拥有getSupervisor :: Person -> History Day Supervisor
和getVP :: Supervisor -> History Day VP
。所以History
的Monad实例是关于组合这些函数的;例如,getSupervisor >=> getVP :: Person -> History Day VP
是为任何Person
获取他们所拥有的VP
历史记录的函数。
嗯,这个History
monad实际上完全与Reader
相同。 History t a
与Reader t a
非常相同(与t -> 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
:
Reader
ask
Reader
执行环境。local
一个很好的比喻是,Reader r a
代表a
,其中包含“洞”,这会阻止您知道我们正在谈论的a
。只有在提供a
来填补漏洞后,您才能获得实际的r
。有很多这样的事情。在上面的示例中,“历史记录”是指定时间之前无法计算的值,超立方体是在指定交集之前无法计算的值,而语言表达式是可以使用的值在提供变量值之前,不要计算。它还为您提供了Reader r a
与r -> a
相同的直觉,因为这样的函数也直观地a
缺少r
。
因此,Functor
的{{1}},Applicative
和Monad
个实例对于您正在对“{{1}”类别进行建模的情况非常有用。 }它缺少Reader
,“并允许您将这些”不完整“对象视为完整对象。
另一种说同样话题的方式:a
消耗r
并生成Reader r a
,r
,a
和{ {1}}实例是使用Functor
的基本模式。 Applicative
=设置Monad
修改另一个Reader
的输出; Functor
=将两个Reader
连接到同一输入并合并其输出; Reader
=检查Applicative
的结果并使用它构建另一个Reader
。 Monad
和Reader
函数=设置Reader
,修改另一个local
的输入。
答案 2 :(得分:19)
在Java或C ++中,您可以从任何地方访问任何变量而不会出现任何问题。当您的代码变为多线程时会出现问题。
在Haskell中,您只有两种方法可以将值从一个函数传递到另一个函数:
fn1 -> fn2 -> fn3
,函数fn2
可能不需要从fn1
传递到fn3
的参数。 Reader monad只传递您想要在函数之间共享的数据。函数可以读取该数据,但不能更改它。这就是读者monad的全部内容。好吧,几乎所有。还有许多功能,例如local
,但这是第一次只能坚持使用asks
。