在 Haskell 中,(f .) . g 意味着什么?

52

我曾经见过很多函数都是按照 (f .) . g 这种模式定义的。例如:

countWhere = (length .) . filter
duplicate  = (concat .) . replicate
concatMap  = (concat .) . map

这是什么意思?


5
(f .) . g 可能也是对原作者代码的巧妙隐晦观点的一部分。 - Marton
1
我不太确定那是什么意思。 - Aadit M Shah
这意味着作者过于聪明,最终写出了难以阅读的代码。 ;) - tibbe
4
((f .). g)x y = f(g x y)的意思是将函数f和g组合起来,并将输入x和y传递给它们。首先,将g应用于x和y,然后将结果传递给f。简而言之,这个等式描述了函数复合的过程。 - fghzxm
2个回答

94

点运算符(即(.))是函数合成运算符。它的定义如下:

infixr 9 .
(.) :: (b -> c) -> (a -> b) -> a -> c
f . g = \x -> f (g x)

如您所见,它需要一个类型为b -> c的函数和另一个类型为a -> b的函数,并返回一个类型为a -> c的函数(即将第一个函数应用于第二个函数的结果)。
函数组合运算符非常有用。它允许您将一个函数的输出导入到另一个函数的输入中。例如,您可以在Haskell中编写一个tac程序,如下所示:
main = interact (\x -> unlines (reverse (lines x)))

不太易读。然而,使用函数组合,您可以将其编写为以下内容:

main = interact (unlines . reverse . lines)

正如您所见,函数组合非常有用,但并不是所有情况下都可以使用它。例如,您无法使用函数组合将filter的输出导入到length中:

countWhere = length . filter -- this is not allowed

这是不允许的原因是因为filter的类型为(a -> Bool) -> [a] -> [a]。将其与a -> b进行比较,我们发现a的类型为(a -> Bool),而b的类型为[a] -> [a]。这导致了类型不匹配,因为Haskell期望length的类型为b -> c(即([a] -> [a]) -> c)。然而,它实际上的类型为[a] -> Int
解决方案非常简单:
countWhere f = length . filter f

然而,有些人不喜欢额外的悬挂的 f。他们更喜欢以 pointfree 风格编写 countWhere,如下所示:
countWhere = (length .) . filter

他们是如何得到这个的?考虑:
countWhere f xs = length (filter f xs)

-- But `f x y` is `(f x) y`. Hence:

countWhere f xs = length ((filter f) xs)

-- But `\x -> f (g x)` is `f . g`. Hence:

countWhere f = length . (filter f)

-- But `f . g` is `(f .) g`. Hence:

countWhere f = (length .) (filter f)

-- But `\x -> f (g x)` is `f . g`. Hence:

countWhere = (length .) . filter

如您所见,(f.) . g 实际上就是 \x y -> f (g x y)。这个概念实际上可以迭代:
f . g             --> \x -> f (g x)
(f .) . g         --> \x y -> f (g x y)
((f .) .) . g     --> \x y z -> f (g x y z)
(((f .) .) .) . g --> \w x y z -> f (g w x y z)

虽然不太美观,但它能完成任务。如果给定两个函数,您也可以编写自己的函数组合运算符:

f .: g = (f .) . g
f .:: g = ((f .) .) . g
f .::: g = (((f .) .) .) . g

使用 (.:) 操作符,您可以这样编写 countWhere 函数:
countWhere = length .: filter

有趣的是,你也可以用点无风格编写(.:)
f .: g = (f .) . g

-- But `f . g` is `(.) f g`. Hence:

f .: g = (.) (f .) g

-- But `\x -> f x` is `f`. Hence:

(f .:) = (.) (f .)

-- But `(f .)` is `((.) f)`. Hence:

(f .:) = (.) ((.) f)

-- But `\x -> f (g x)` is `f . g`. Hence:

(.:) = (.) . (.)

同样地,我们得到:
(.::)  = (.) . (.) . (.)
(.:::) = (.) . (.) . (.) . (.)

正如您所看到的,(.:)(.::)(.:::) 只是 (.) 的幂(即它们是 (.)迭代函数)。对于数学中的数字:
x ^ 0 = 1
x ^ n = x * x ^ (n - 1)

同样地,对于数学中的函数:
f .^ 0 = id
f .^ n = f . (f .^ (n - 1))

如果f(.),那么:
(.) .^ 1 = (.)
(.) .^ 2 = (.:)
(.) .^ 3 = (.::)
(.) .^ 4 = (.:::)

这篇文章接近尾声。最后一个挑战是让我们用点自由风格来编写以下函数:
mf a b c = filter a (map b c)

mf a b c = filter a ((map b) c)

mf a b = filter a . (map b)

mf a b = (filter a .) (map b)

mf a = (filter a .) . map

mf a = (. map) (filter a .)

mf a = (. map) ((filter a) .)

mf a = (. map) ((.) (filter a))

mf a = ((. map) . (.)) (filter a)

mf = ((. map) . (.)) . filter

mf = (. map) . (.) . filter

我们可以进一步简化如下:
compose f g = (. f) . (.) . g

compose f g = ((. f) . (.)) . g

compose f g = (.) ((. f) . (.)) g

compose f = (.) ((. f) . (.))

compose f = (.) ((. (.)) (. f))

compose f = ((.) . (. (.))) (. f)

compose f = ((.) . (. (.))) (flip (.) f)

compose f = ((.) . (. (.))) ((flip (.)) f)

compose = ((.) . (. (.))) . (flip (.))

使用 compose,您现在可以将 mf 编写为:

mf = compose map filter

虽然它看起来有点丑陋,但这是一个非常棒的令人惊叹的概念。现在,您可以将任何形式为\x y z -> f x (g y z)的函数写成compose f g,这非常整洁。


4
形如 (.) ^ i 的表达式不是良类型的,因此它们实际上不是有效的 Haskell 代码。 - Tom Ellis
1
是的。然而,我写了“同样适用于数学函数:”,由于这是一个数学解释,我认为使用 ^ 代替数字是可以的。尽管如此,我会将运算符更改为 .^ 以区分两者。 - Aadit M Shah
10
+1 - 回答清晰明了。然而,我完全不同意点无表示法更易读或在任何方面更好的观点。countWhere f xs = length (filter f xs) 比其他所有版本都要可读得多,因为它能够立即清楚地表明:1) 函数有多少个参数,2) 它接受什么样的参数(通过“聪明”的名称),3) 几乎任何程序员都可以在几秒钟内阅读并有一些直觉上的了解所发生的事情。当表达式变得比简单的组合更复杂时,弄清点无表示法的版本需要更多的时间。 - Bakuriu
1
@Bakuriu 我从未提到 pointfree 风格在任何方面都更可读或更好。我提到“可读”的唯一时候是与 tac 示例相关联的。我同意,人们可能会过分迷恋 pointfree 编程。用 pointfree 风格编写的 mf 函数就是这种情况的范例。尽管如此,点无风格编程很有趣,从数学角度研究它可以使程序的结构更具数学可处理性。它可以让你的代码更短,但只有在有限的情况下。 - Aadit M Shah
3
经过五年的Haskell学习,我终于因阅读这个答案而理解了pointfree。 - polkovnikov.ph
显示剩余2条评论

13

这是一个关于品味的问题,但我觉得这种风格不太好看。首先我会解释一下它的含义,然后我会提出一个我更喜欢的替代方案。

你需要知道 (f . g) x = f (g x)(f ?) x = f ? x 对于任何运算符 ?都成立。从中我们可以推断出:

countWhere p = ((length .) . filter) p
              = (length .) (filter p)
              = length . filter p

所以

countWhere p xs = length (filter p xs)

我更喜欢使用一个名为.:的函数。

(.:) :: (r -> z) -> (a -> b -> r) -> a -> b -> z
(f .: g) x y = f (g x y)

那么 countWhere = length .: filter。个人认为这样更清晰明了。

(.:Data.Composition 中被定义,可能也在其他地方被定义。)


你也可以将 (.:) 定义为 (.:) = fmap fmap fmap。这更加通用,因为你可以用于任何函子。例如,你可以执行 (* 2) .: Just [1..5]。当然,你需要给它正确的类型签名:(.:) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b) - Aadit M Shah
7
在这种情况下,我更喜欢类似 <$$> = fmap . fmap 的写法,因为 .: 按照惯例被专门用于 (->) r,而且 "outer" fmap 已经在 (->) r 函子上了。 - kqr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接