9

阅读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)

我很困惑。为什么?这有什么用?

或者,更准确地说:在我看来,这似乎只是将函数的功能复制为应用程序,但语法更冗长。

那么,你能给我举个例子,说明 Reader monad 能做到 applicatives 不能做到的吗?

实际上,我还想问这两个中的任何一个有什么用:应用函数或 Reader monad - 因为虽然能够将相同的参数应用于四个函数(fa, fb, fc, fd)而不用重复这个参数四次确实减少了一些重复性,我不确定这一微小的改进是否能证明这种复杂程度;所以我想我一定错过了一些突出的东西;但这值得一个单独的问题

4

2 回答 2

11

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)
    
于 2019-04-22T16:24:25.420 回答
5

在回答关于 的主要问题之前Reader,我将首先对 applicative-versus-monad 进行一些评论。虽然这种应用风格的表达......

g <$> fa <*> fb

...确实相当于这个do-block ...

do
    x <- fa
    y <- fb
    return (g x y)

...从 切换ApplicativeMonad可以根据其他计算的结果来决定要执行哪些计算,或者换句话说,可以产生取决于先前结果的效果(另请参见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对函数仿函数做了什么很有趣,然后弄清楚为什么这样做是有意义的。(可以在此问答中找到解决方案。)

于 2019-04-23T04:00:51.003 回答