【问题标题】:Why does <$> and <*> take input in an order opposite of >>=?为什么 <$> 和 <*> 以与 >>= 相反的顺序接受输入?
【发布时间】:2014-11-18 20:42:00
【问题描述】:

我理解 &lt;$&gt; 的类型签名背后的原因,因为它只是 fmap 的中缀版本,但将其与 &gt;&gt;= 的类型签名相比,对我来说意义不大。

让我们首先确定我的意思。

(>>=) :: Monad m => m a -> (a -> m b) -> m b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
(<$>) :: Functor f => (a -> b) -> f a -> f b

查看类型签名,我们可以看到 &gt;&gt;= 在左侧接受一个值,在右侧接受一个函数,如果考虑它的链接属性,这很有意义:foo &gt;&gt;= bar &gt;&gt;= baz

这让我想知道,为什么 &lt;*&gt;&lt;$&gt; 不这样做呢?你不能写foo &lt;*&gt; bar &lt;*&gt; baz,因为它要求foo &lt;*&gt; bar的输出是一个函数,而不是一个值。

我知道&lt;**&gt;=&lt;&lt; 存在,它们都颠倒了参数的顺序,允许我做类似的事情:

Just 4 <**> pure (+3) <**> pure (*2) >>= (\x -> Just (x-3)) 

这本可以完美地简化为:

Just 4 <$$> (+3) <$$> (*2) >>= (\x -> Just (x-3))

如果&lt;$$&gt; 已经存在,或者&lt;$&gt;&lt;*&gt; 的参数顺序颠倒了。

让我想知道为什么存在差异的另一件事是,新手更难习惯和/或记住它是函数还是首先出现的值,而无需查找它。

那么为什么&lt;*&gt;&lt;$&gt;fn op val&gt;&gt;=val op fn

【问题讨论】:

  • 但你经常想要 Applicatives 完全一样:例如 (+) &lt;$&gt; Just 5 &lt;*&gt; Just 5 Just 漂亮吗;)
  • (&gt;&gt;=) 构造链“命令式”(否则我们会想知道为什么它与$ 的顺序不同)。 (&lt;=&lt;) 还允许链接 - “(.) 样式”。 (&lt;*&gt;) 对于“应用”多参数函数很有用——按照声明的顺序提供参数。
  • 真正的问题是为什么(&gt;&gt;=) 使用与 Haskell 中几乎所有内容相反的顺序。 (=&lt;&lt;) :: Monad m =&gt; (a -&gt; m b) -&gt; (m a -&gt; m b) 的类型看起来比 (&gt;&gt;=) = flip (=&lt;&lt;) 的类型更像 Haskell 中的其他所有内容。
  • @Rhymoid 我想从左到右阅读更自然,所以通过箭头方向从左到右将值传递给函数感觉更自然? val &gt;&gt;= fun
  • @ElectricCoffee 从左到右,从上到下是not natural per se。在文件中,“从早到晚”是一个更合适的术语。在这种情况下,像(&gt;&gt;&gt;) 那样进行组合工作似乎是有意义的。甚至类型更好(仅限于函数,(&gt;&gt;&gt;) :: (a -&gt; b) -&gt; (b -&gt; c) -&gt; (a -&gt; c))。为了使这更自然,您还需要更改函数应用程序的工作方式:(foo &gt;&gt;&gt; bar &gt;&gt;&gt; baz) x == baz (bar (foo x))。你确定要改写x (foo &gt;&gt;&gt; bar &gt;&gt;&gt; baz) == ((x foo) bar) baz吗?

标签: haskell parameters monads functor applicative


【解决方案1】:

“为什么要按这个顺序取参数”的答案基本上是“因为”。定义这些函数的人认为这是最好的方法。 (而且在每种情况下可能不是同一个人。)但是,我将提供一些示例:

假设我们有某种解析单子。还假设我们已经定义了

 data Foobar = Foobar X Y Z

 parseFoo :: Parser X
 parseBar :: Parser Y
 parseBaz :: Parser Z

那么我们可以写

 parseFoobar :: Parser Foobar
 parseFoobar = do
   foo <- parseFoo
   bar <- parseBar
   baz <- parseBaz
   return (Foobar foo bar baz)

或者,明确地,

parseFoobar =
  parseFoo >>= \ foo ->
  parseBar >>= \ bar ->
  parseBaz >>= \ baz ->
  return (Foobar foo bar baz)

现在让我们编写应用程序样式:

parseFoobar = return Foobar <*> parseFoo <*> parseBar <*> parseBaz

或者,或者,

parseFoobar = Foobar <$> parseFoo <*> parseBar <*> parseBaz

如果我们假设&lt;**&gt; = flip &lt;*&gt; 存在(并且具有正确的关联性),那么我们有

parseFoobar = parseBaz <**> parseBar <**> parseFoo <**> return Foobar

这看起来很奇怪。函数在末尾,参数以相反的顺序排列?为什么要这样写? (请注意,任何效果都以相反的顺序排列。)

在单子版本中,效果从上到下发生。在应用版本中,效果从左到右发生。这看起来很“自然”。

【讨论】:

  • 你能在示例函数中添加一些示例类型吗?只是这样我可以更轻松地了解正在发生的事情
  • 编辑了一些东西。
  • 这在函数链方面是如何工作的?您在这里所做的是将一个函数应用于 3 个包装值,但 Just 4 &gt;&gt;= foo &gt;&gt;= bar &gt;&gt;= baz 的工作方式与 (+) &lt;$&gt; Just 3 &lt;*&gt; Just 4 的工作方式大不相同
  • 实际上是三个解析器来解析一些输入并产生值作为输出,而不是三个“包装值”。在这两个版本中,您都在执行产生输出的有效计算(解析器),然后将这些输出组合成一个数据结构 (Foobar)。
【解决方案2】:

不要让 monad 妨碍这里。想想应用程序。

比较:

(<$>) :: Functor f => (a -> b) -> f a -> f b

($) :: (a -> b) -> a -> b

您可以看到常规应用程序和函子下的应用程序之间存在联系。

琐事:已经有proposals 使用括号来重载空白(应用程序),以便我们可以编写:

(| f a1 .. an |)

而不是

pure f <*> a1 <*> .. <*> an

【讨论】:

  • 我从没见过这样的,我只从fmap 的角度看&lt;$&gt;,现在你已经将它作为$ 的变体呈现出来,它更有意义
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-05
  • 1970-01-01
  • 2016-07-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多