已阅读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)
U。我花了很多时间来理解为什么会这样,但是-在一张有我的笔记的纸上,我应该能够证明这一点。
然后我读了http://learnyouahaskell.com/for-a-few-monads-more#reader。我们这次以monadic语法再次回到了这些东西:
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)
我很困惑。为什么?这有什么用?
或者,更精确地说:在我看来,这只是复制函数的功能作为应用程序,但语法更为冗长。
那么,您能举一个例子吗,Reader monad可以像应用程序那样起作用吗?
实际上,我还想问一下这两个函数的用途:应用函数或Reader monad-因为能够将相同的参数应用于四个函数(fa
,{ {1}},fb
,fc
)没有重复此参数四次确实会减少一些重复性,我不确定这种微小的改进是否足以证明这种复杂性;因此,我认为我必须错过一些突出的东西;但这值得一个单独的问题
答案 0 :(得分:8)
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)
答案 1 :(得分:4)
在开始提出关于Reader
的主要问题之前,我将首先简要介绍一下applicative-versus-monad。虽然这个适用的样式表达...
g <$> fa <*> fb
...确实等同于此do-block ...
do
x <- fa
y <- fb
return (g x y)
...从Applicative
切换到Monad
使得可以基于其他计算的结果来决定要执行哪些计算,或者换句话说,具有取决于先前计算的效果结果(另请参见chepner's answer):
do
x <- fa
y <- if x >= 0 then fb else fc
return (g x y)
尽管Monad
比Applicative
更强大,但我建议不要认为它比另一个更有用。首先,因为有些应用函子不是单子。其次,因为不使用比您实际需要更多的功率,这会使整体事情变得更简单。 (此外,这种简单性有时可以带来明显的好处,例如an easier time dealing with concurrency。)
带括号的注释:当涉及applicative-versus-monad时,Reader
是一个特例,因为Applicative
和Monad
实例happen to be equivalent。对于函数函子(即((->) r)
,即没有新类型包装器的Reader r
),我们有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
但是,使用应用样式,我们可以立即根据两个函数(或者我倾向于说两个 properties )来编写它,而不必注意最终的论点是:
(&&) <$> isUpper <*> isDigit
对于一个如此小的示例,是否以这种方式编写并不重要,很大程度上取决于口味–我很喜欢它。其他人受不了了。不过,重点是,有时候我们并不特别关注某个值是一个函数,因为我们碰巧将其视为其他事物(在这种情况下,它是一种属性),而事实上它最终是在我们看来,功能可能只是实现细节。
关于这种观点转变的一个非常引人注目的示例涉及应用程序范围内的配置参数:如果程序某个层上的每个函数都将某个Config
值用作参数,那么您可能会发现它更舒适地对待其参数可用性作为背景假设,而不是在任何地方明确传递。事实证明,这是读者monad的主要用例。
无论如何,您对Reader
有用性的怀疑至少以一种方式得到了证明。事实证明,Reader
本身(但包装在一个花哨的新类型函子中)实际上在野外并不经常使用。 极为常见的是通常通过ReaderT
和/或MonadReader
类的方式结合了Reader
功能的单子堆栈。在这个答案的范围内,冗长地讨论monad转换器是一个题外话,所以我只注意到您可以像使用ReaderT r IO
一样使用Reader r
,除了您也可以沿途进行IO
计算。将ReaderT
之上的IO
的某些变体作为Haskell应用程序外层的核心类型并不罕见。
最后,您可能会发现有趣的是,观察join
中的Control.Monad
对函数函子的作用,然后弄清楚为什么这样做是有意义的。 (可以在this Q&A中找到解决方案。)