Haskell 应用习语?

Posted

技术标签:

【中文标题】Haskell 应用习语?【英文标题】:Haskell Applicative idiom? 【发布时间】:2015-12-05 13:13:20 【问题描述】:

我是 Haskell 的新手,对如何以最惯用和最清晰的方式最好地表达一些操作感到困惑。目前(还会有更多)我对<*> 感到困惑(我什至不知道该叫什么)。

例如,如果我有,说

f = (^2)
g = (+10)

作为代表函数(实际上它们更复杂,但这里的关键是它们是不同的和不同的),那么

concatMap ($ [1,2,3,4,10]) [(f <$>), (g <$>) . tail . reverse] 

concat $ [(f <$>), (g <$>) . tail . reverse] <*> [[1,2,3,4,10]]

完成同样的事情。

是这些更惯用的 Haskell 之一,其中一个是否暗示了一个有经验的 Haskell 读者而另一个没有。也许还有其他(更好)的方式来表达完全相同的东西。像我这样的 Haskeller 新手可能会遗漏这两种方法之间的概念差异吗?

【问题讨论】:

您的输入是否只会成为一个列表?如果是这样,显然前者更好。输入是指数字列表。 @itsbruce:好点子。是的,只有一个列表。 将列表的ApplicativeFunctor 实例与concat 组合起来感觉非常可疑。 Functor+return+join=Monad,对于列表,concat=join。因此,如果您要映射然后连接,concatMap=&lt;&lt; 可能更简洁。 @dfeuer:听起来很有启发性(如果我理解的话;记住:初学者)。你能详细说明一下吗? 他是最好的答案。 【参考方案1】:

您的函数(f &lt;$&gt;)(g &lt;$&gt;).tail.reverse 都返回一个幺半群类型(在本例中为列表),因此您可以使用mconcat 将它们转换为单个函数。然后您可以将此函数直接应用于输入列表,而不是将其包装在另一个列表中并使用concatMap

mconcat [(f <$>), (g <$>).tail.reverse] $ [1,2,3,4,10]

为了对此进行扩展,如果b 是一个幺半群,则函数a -&gt; bMonoid 的一个实例。此类功能的mappend 的implementation 是:

mappend f g x = f x `mappend` g x

或等效

mappend f g = \x -> (f x) `mappend` (g x)

所以给定两个函数fg,它们返回一个幺半群类型bfmappendg 返回一个函数,该函数将其参数应用于fg,并使用以下方法组合结果Monoidb 实例。

mconcat 的类型为 Monoid a =&gt; [a] -&gt; a,并使用 mappend 组合输入列表的所有元素。

列表是幺半群,其中mappend == (++) 所以

mconcat [(f <$>), (g <$>).tail.reverse]

返回一个类似的函数

\x -> (fmap f x) ++ (((fmap g) . tail . reverse) x)

【讨论】:

这很有趣。我确实花了一些时间来理解,但the source for the function instance of Monoid 解决了问题。 (f &lt;$&gt;) 的计算结果为 ([f] &lt;*&gt;),其类型为 [a] -&gt; [a],其返回类型 [a]Monoid 的实例,使函数本身成为 Monoid 的实例。 我说[a] -&gt; [a] 但你对Num 约束是正确的,它也适用于这里(我很草率并省略了它)。 mconcatmappend 散布一个列表,所以我们得到mconcat [(f &lt;$&gt;), (g &lt;$&gt;) . tail . reverse] = \x -&gt; (([f] &lt;$&gt;) x) `mappend` (((g &lt;$&gt;) . tail . reverse) x),所以列表中的所有函数都提供了mconcat func 提供的参数,然后这些函数的输出与mappend 连接。在我们的例子中,函数输出列表为mappend = (++),对于其他幺半群,将使用相应的mappend 这对我来说并不明显,我只是在检查了 Monoid 实例的功能后才知道的。尽管如此,我认为没有很多其他方法可以有意义地实现实例,所以从这个意义上说,我会说这是“显而易见的”实现。我同意它看起来很奇怪、很酷且功能强大,我个人不会在我希望其他人稍后阅读的代码中快速使用它。 我说我不会在我希望人们阅读的代码中使用它太快了。我可能会遇到这种模式准确表达我打算做的事情的情况。 (也许我会添加评论以澄清发生了什么;)) @raxacoricofallapatorius - 只有当返回类型 b 是一个幺半群时,函数才是幺半群,请参阅更新。【参考方案2】:

我个人会写你的例子

f = (^2)
g = (+10)

let xs = [1,2,3,4,10]
in (map f xs) ++ (map g . tail $ reverse xs)

在一种非常适用的“心情”中,我会将in之后的部分替换为

((++) <$> map f <*> map g . tail . reverse) xs

在这种情况下,我实际上并不认为它更具可读性。如果您不直接理解它的含义,请花一些时间来理解 Applicative 的实例 ((-&gt;) a) (Reader)。

我认为选择实际上取决于您想要做什么,即您的输出应该意味着什么。在您的示例中,任务非常抽象(基本上只是展示了 Applicative 可以做什么),因此使用哪个版本并不是直接显而易见的。

[]Applicative 实例直观地与组合相关,所以我会在这样的情况下使用它:

-- I want all pair combinations of 1 to 5
(,) <$> [1..5] <*> [1..5]

如果您有许多函数,并且您想尝试这些函数与多个参数的所有组合,我确实会使用Applicative[] 实例。但是,如果您所追求的是不同转换的串联,我会这样写(我在上面做了)。

作为一个中等经验的 Haskeller,我只有 2 美分。

【讨论】:

【参考方案3】:

我有时会遇到类似的问题。你有一个元素,但有多个功能。

通常我们有多个元素和单个功能:所以我们这样做:

map f xs

但这不是 Haskell 中的问题。对偶很简单:

map ($ x) fs

事实上,你的x实际上是一个列表,你想在map之后concat,所以你这样做

concatMap ($ xs) fs

我无法真正理解第二个等式中发生了什么,即使我可以使用应用定律推断它与第一个等式的作用相同。

【讨论】:

以上是关于Haskell 应用习语?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 emacs - haskell 模式下运行 haskell 应用程序?

Haskell 中的函数应用

如何用 Haskell 编写 Windows 服务应用程序?

Haskell 多线程有多难?

如何在整个 Web 应用程序堆栈中利用 Haskell 类型安全性?

Haskell中函数应用运算符的使用