阅读 http://learnyouahaskell.com/functors-applicative-functors-and-monoids#applicative-functors 后,我可以提供一个使用函数作为应用函子的示例:
假设res
是 4 个参数的函数,fa
、fb
、fc
、fd
都是接受单个参数的函数。然后,如果我没记错的话,这个应用表达式:
f <$> fa <*> fb <*> fc <*> fd $ x
意思与这个非花哨的表达相同:
f (fa x) (fb x) (fc x) (fd x)
呸。我花了相当多的时间来理解为什么会这样,但是 - 在一张纸和我的笔记的帮助下 - 我应该能够证明这一点。
然后我读了 http://learnyouahaskell.com/for-a-few-monads-more#reader.我们又回到了这个东西,这次是一元语法:
do
a <- fa
b <- fb
c <- fc
d <- fd
return (f a b c d)
虽然我需要另一张A4纸币来证明这一点,但我现在非常有信心,这再次意味着相同:
f (fa x) (fb x) (fc x) (fd x)
我很困惑。为什么?这有什么用?
或者,更准确地说:在我看来,这似乎只是复制了函数作为应用程序的功能,但语法更冗长。
那么,你能给我举个例子,说明读者 monad 可以
做到应用不能做到的功能吗?实际上,我还想问一下这两个中的任何一个有什么用:应用函数或读者 monad - 因为虽然能够将相同的参数应用于四个函数(fa
、fb
、fc
、fd
)而不重复这个论点四次确实减少了一些重复性,我不确定这种微小的改进是否证明了这种复杂程度的合理性; 所以我一定错过了一些突出的东西, 我认为;但这值得一个单独的问题
monadic 版本允许您在调用上下文中找到的函数之间添加额外的逻辑,甚至决定根本不调用它们。
do
a <- fa
if a == 3
then return (f a 1 1 1)
else do
b <- fb
c <- fc
d <- fd
return (f a b c d)
在您的原始do
表达式中,您确实没有执行Applicative
实例无法执行的任何操作,实际上,编译器可以确定这一点。如果使用ApplicativeDo
扩展名,则
do
a <- fa
b <- fb
c <- fc
d <- fd
return (f a b c d)
确实会脱糖f <$> fa <*> fb <*> fc <*> fd
而不是fa >>= a -> fb >>= b -> fc >>= c -> fd >>= d -> return (f a b c d)
.
这也适用于其他类型的情况,例如
Maybe
:f <$> (Just 3) <*> (Just 5) == Just (f 3 5) == do x <- Just 3 y <- Just 5 return (f 3 5)
[]
:f <$> [1,2] <*> [3,4] == [f 1 3, f 1 4, f 2 3, f 2 4] == do x <- [1,2] y <- [3,4] return (f x y)
在讨论你关于Reader
的主要问题之前,我将从一般的应用与单体的一些评论开始。虽然这种应用风格的表达...
g <$> fa <*> fb
。确实相当于这个做块...
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
更强大,但我建议不要认为一个比另一个更有用。首先,因为有些应用函子不是单子;其次,因为不使用比实际需要更多的功率往往会使事情整体更简单。(此外,这种简单性有时可以带来实实在在的好处,例如更容易处理并发性。
括号说明:当涉及到应用与单体时,Reader
是一个特例,因为Applicative
和Monad
实例恰好是等效的。对于函数函子(即((->) r)
,Reader r
没有 newtype 包装器),我们有m >>= f = flip f <*> m
.这意味着,如果采用我上面写的第二个 do-block(或 chepner 答案中的类似块等),并假设正在使用的 monad是Reader
,我们可以将其转换为应用风格。
尽管如此,Reader
最终是一件如此简单的事情,我们为什么要在这种特定情况下为上述任何一项而烦恼呢?这里有一些建议。
首先,Haskellers 经常对裸函数函子保持警惕,((->) r)
,这是可以理解的:与直接应用函数的"非花哨表达式"相比,它很容易导致不必要的神秘代码。不过,在少数特定情况下,使用起来很方便。举个小例子,考虑Data.Char
中的这两个函数:
isUpper :: Char -> Bool
isDigit :: Char -> Bool
现在假设我们要编写一个函数来检查字符是大写字母还是 ASCII 数字。直接的做法是
:c -> isUpper c && isDigit c
但是,使用应用样式,我们可以立即根据两个函数(或者,我倾向于说,两个属性)来编写它,而不必注意最终的论点在哪里:
(&&) <$> isUpper <*> isDigit
像这样小的例子,用这种方式写也没什么大不了的,很大程度上取决于品味——我挺喜欢的,别人受不了。不过,关键是,有时我们并不特别关心某个值是一个函数,因为我们碰巧把它看作是别的东西——在这种情况下,是一个属性——而它最终是一个函数的事实对我们来说可能只是一个实现细节。
这种视角转变的一个非常引人注目的例子涉及应用程序范围的配置参数:如果程序某个层的每个函数都以一些Config
值作为参数,那么您可能会发现它更愿意将其可用性视为背景假设,而不是在任何地方显式传递它。事实证明,这是阅读器monad的主要用例。
无论如何,你对Reader
有用性的怀疑至少以一种方式得到了证实。事实证明,Reader
本身,但包裹在花哨的新型函子中的函数,实际上并不经常在野外使用。非常常见的是包含Reader
功能的一元堆栈,通常通过ReaderT
和/或MonadReader
类。详细讨论单元变压器对于这个答案的空间来说太离题了,所以我只想指出,例如,你可以使用,ReaderT r IO
就像你使用Reader r
一样,除了你也可以在此过程中滑入IO
计算。将ReaderT
的某种变体视为Haskell应用程序外层的核心类型并不罕见IO
。
最后,您可能会发现有趣的是,看看Control.Monad
的join
对函数函子的作用,然后弄清楚为什么这是有意义的。(解决方案可以在此问答中找到。