【问题标题】:What can the Reader monad do that applicative functions cannot?Reader monad 能做哪些应用函数不能做的事情?
【发布时间】:2019-09-11 19:47:20
【问题描述】:

阅读了http://learnyouahaskell.com/functors-applicative-functors-and-monoids#applicative-functors,我可以提供一个使用函数作为应用函子的例子:

假设 res 是一个有 4 个参数的函数,fafbfcfd 都是采用单个参数的函数。那么,如果我没记错的话,这个适用的表达方式:

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 - 因为虽然能够将相同的参数应用于四个函数(fafb , fc, fd) 不重复这个论点四次确实减少了一些重复性,我不确定这个微小的改进是否能证明这种复杂程度的合理性;所以我想我一定错过了一些突出的东西;但这值得一个单独的问题

【问题讨论】:

  • 不,它等同于f (fa x) (fb x) (fc x) (fd x)do 符号是语法糖,这意味着你写了fa &gt;&gt;= \a -&gt; fb &gt;&gt;= \b -&gt; fc &gt;&gt;= \c -&gt; fd &gt;&gt;= \d -&gt; return (f a b c d)。请注意,&gt;&gt;= 取决于 monad 会做不同的事情。
  • @WillemVanOnsem 是的,我知道 do 符号是你所写内容的同步糖;但是,在填写了一张 A4 纸后,我有理由确定在 READER MONAD 的情况下(不是任何 monad)fa &gt;&gt;= \a -&gt; fb &gt;&gt;= \b -&gt; fc &gt;&gt;= \c -&gt; fd &gt;&gt;= \d -&gt; return (f a b c d) 实际上等同于f (fa x) (fb x) (fc x) (fd x)
  • 这是一个公平的问题,但你可以对许多 monad 提出同样的问题——你可以用 monad 做的一些事情也可以用 applicative 来做。但相当多的不能(遗憾的是,我没有一个很好的例子,但我相信会有比我更专业的人),不幸的是 LYAH 选择了一个可能是的例子而是以应用表示法完成。
  • Reader a monad 和 (-&gt;) a monad 是同构的。它们之间的区别只是一个包装构造函数。你可以根据“口味”来选择,可以这么说。我猜Reader a 使用得更多,因为它有一个名字,而且包装有助于理解什么是单子函数,什么是常规函数。
  • 虽然一般MonadApplicative 更强大(如chepner 的回答中所解释的),Reader/function 函子是一个非常特殊的情况,其中Monad 和@987654350 @ 实例恰好是等价的——参见this answer of mine 的第二部分。 (抄送@RobinZigmond)

标签: haskell monads applicative reader-monad


【解决方案1】:

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 &lt;$&gt; fa &lt;*&gt; fb &lt;*&gt; fc &lt;*&gt; fd 而不是fa &gt;&gt;= \a -&gt; fb &gt;&gt;= \b -&gt; fc &gt;&gt;= \c -&gt; fd &gt;&gt;= \d -&gt; 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)
    

【讨论】:

  • 值得注意的是Readerthe instances are actually equivalent的一个特例。
  • 这很好地解释了为什么do 符号比应用符号更有用,但它没有涉及为什么应用符号比“非花哨”更有用符号。
  • 在这件事上,除了“它在适用时更简洁”之外,我没有什么比这更聪明(或不那么尖刻)的话了。
【解决方案2】:

在回答您关于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)

虽然MonadApplicative 更强大,但我建议不要认为它比另一个更有用。首先,因为有些应用函子不是单子;其次,因为不使用比实际需要更多的功率往往会使事情变得更简单。 (此外,这种简单有时可以带来实实在在的好处,例如an easier time dealing with concurrency。)


附注:当涉及到 applicative-versus-monad 时,Reader 是一个特例,因为 ApplicativeMonad 实例 happen to be equivalent。对于函数函子(即((-&gt;) r),即没有newtype 包装器的Reader r),我们有m &gt;&gt;= f = flip f &lt;*&gt; m。这意味着,如果采用我刚刚在上面写的第二个 do-block(或 chepner 的答案中类似的一个,等等)并且假设正在使用的 monad 是Reader,我们可以将其转换为应用风格。


尽管如此,Reader 最终是一件如此简单的事情,在这种特定情况下,我们为什么还要为上述任何事情烦恼呢?这里有一些建议。

首先,Haskellers 经常对裸函数函子 ((-&gt;) r) 持谨慎态度,这是可以理解的:与函数在其中的“非花哨表达式[s]”相比,它很容易导致不必要的神秘代码直接申请。不过,在某些特定情况下,它可以很方便地使用。举个小例子,考虑Data.Char中的这两个函数:

isUpper :: Char -> Bool
isDigit :: Char -> Bool

现在假设我们要编写一个函数来检查一个字符是大写字母还是 ASCII 数字。最直接的做法是:

\c -> isUpper c && isDigit c

不过,使用 applicative 风格,我们可以立即根据两个函数来编写它——或者,我倾向于说,两个 属性——而不必注意最终的论点是:

(&&) <$> isUpper <*> isDigit

这么小的例子,要不要这样写也没什么大不了的,主要看个人口味——我挺喜欢的;别人受不了。不过,关键是有时我们并不特别关心某个值是否是一个函数,因为我们碰巧将它视为其他东西——在这种情况下,是一个属性——而事实上它最终是一个函数在我们看来只是一个实现细节。

这种观点转变的一个非常引人注目的例子涉及应用程序范围的配置参数:如果跨程序某个层的每个函数都将一些 Config 值作为参数,那么您可能会发现将其可用性视为更舒服背景假设,而不是到处明确地传递它。事实证明,这是 reader monad 的主要用例。


无论如何,您对Reader 有用性的怀疑至少在某种程度上得到了证明。事实证明,Reader 本身,functions-but-wrapped-in-a-fancy-newtype 仿函数,实际上并没有在野外经常使用。 非常常见的是包含Reader 功能的一元堆栈,通常通过ReaderT 和/或MonadReader 类的方式。详细讨论 monad 转换器对于这个答案的空间来说太过分了,所以我只想指出,你可以使用 ReaderT r IO 就像你使用 Reader r 一样,除了你也可以滑倒在IO 计算中。看到ReaderT 的一些变体而不是IO 作为Haskell 应用程序外层的核心类型并不罕见。


最后一点,您可能会发现看看Control.Monad 中的join 对函数函子做了什么很有趣,然后弄清楚为什么这样做是有意义的。 (可以在this Q&A找到解决方案。)

【讨论】:

  • @FabianSchneider 我重写了您的编辑,因为我觉得,就文本的流动而言,部分答案不太适合那种冗长的题外话。在那里添加更具体的插图的想法很好;感谢那。 (另一方面:应用表达式可能会产生影响,甚至是副作用,如果我们谈论的是 IO 类似的事情。我宁愿描述单子方面的关键区别,允许影响取决于计算结果。)
  • 似乎合理;是的,它们可以产生影响;我应该在较长的文本版本中也提到这一点:)这确实是对差异的非常好的描述。
  • 我认为这里的另一个答案是非答案。
猜你喜欢
  • 1970-01-01
  • 2017-03-05
  • 2011-04-12
  • 2017-07-13
  • 1970-01-01
  • 2017-10-13
  • 2013-01-21
  • 2019-11-08
  • 1970-01-01
相关资源
最近更新 更多