在回答关于 的主要问题之前Reader,我将首先对 applicative-versus-monad 进行一些评论。虽然这种应用风格的表达......
g <$> fa <*> fb
...确实相当于这个do-block ...
do
x <- fa
y <- fb
return (g x y)
...从 切换Applicative到Monad可以根据其他计算的结果来决定要执行哪些计算,或者换句话说,可以产生取决于先前结果的效果(另请参见chepner 的回答):
do
x <- fa
y <- if x >= 0 then fb else fc
return (g x y)
虽然Monad比 更强大Applicative,但我建议不要认为它比另一个更有用。首先,因为有些应用函子不是单子;其次,因为不使用比实际需要更多的功率往往会使事情变得更简单。(此外,这种简单有时可以带来实实在在的好处,例如更容易处理并发。)
附带说明:当涉及到 applicative-versus-monad 时,Reader是一种特殊情况,因为ApplicativeandMonad实例恰好是等价的。对于函数仿函数(即((->) r),它Reader r没有 newtype 包装器),我们有m >>= f = flip f <*> m. 这意味着如果采用我在上面写的第二个 do-block(或 chepner 的答案中类似的一个等)并假设正在使用的 monad 是Reader,我们可以将其转换为应用风格。
尽管如此,由于Reader最终是一件如此简单的事情,在这种特定情况下,我们为什么还要为上述任何事情烦恼呢?这里有一些建议。
首先,Haskellers 经常对裸函数函子 保持警惕,((->) r)这是可以理解的:与直接应用函数的“非花哨表达式[s]”相比,它很容易导致不必要的神秘代码。不过,在某些特定情况下,它可以很方便地使用。举个小例子,考虑以下两个函数Data.Char:
isUpper :: Char -> Bool
isDigit :: Char -> Bool
现在假设我们要编写一个函数来检查一个字符是大写字母还是 ASCII 数字。最直接的做法是:
\c -> isUpper c && isDigit c
但是,使用应用风格,我们可以立即根据两个函数来编写它——或者,我倾向于说,两个属性——而不必注意最终参数的去向:
(&&) <$> isUpper <*> isDigit
就这么个小例子,要不要这样写也没什么大不了的,主要看个人口味——我挺喜欢的;别人受不了。不过,关键是有时我们并不特别关心某个值是否是一个函数,因为我们碰巧将它视为其他东西——在这种情况下,是一个属性——而事实上它最终是一个函数对我们来说可能只是一个实现细节。
这种观点转变的一个非常引人注目的例子涉及应用程序范围的配置参数:如果程序的某个层中的每个单个函数都将某个Config值作为参数,那么您可能会发现将其可用性作为背景假设而不是到处明确地传递它。事实证明,这是 reader monad 的主要用例。
无论如何,您对 的 有用性的怀疑Reader至少在某种程度上得到了证明。事实证明,Readerfunctions-but-wrapped-in-a-fancy-newtype functor 本身实际上并没有在野外经常使用。非常常见的是包含 的功能的一元堆栈Reader,通常通过ReaderT和/或MonadReader类的方式。详细讨论 monad 转换器对于这个答案的空间来说太过分了,所以我只想指出,例如,你可以ReaderT r IO像使用 一样使用Reader r,除了你也可以在IO计算过程中滑倒。ReaderT看到over的一些变体并不罕见IO作为 Haskell 应用程序外层的核心类型。
最后一点,你可能会发现看看joinfromControl.Monad对函数仿函数做了什么很有趣,然后弄清楚为什么这样做是有意义的。(可以在此问答中找到解决方案。)