【问题标题】:What does (f .) . g mean in Haskell?什么是 (f .) 。 g 在 Haskell 中是什么意思?
【发布时间】:2013-11-29 05:57:44
【问题描述】:

我看到很多函数都是根据(f .) . g 模式定义的。例如:

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

这是什么意思?

【问题讨论】:

  • (f.)。 g 也可能是对原作者代码的巧妙掩饰意见的一部分。
  • 我不太确定这是什么意思。
  • 这意味着作者太聪明了,最终写出了不可读的代码。 ;)
  • ((f .) . g) x y = f (g x y)

标签: haskell functional-programming pointfree function-composition tacit-programming


【解决方案1】:

点运算符(即(.))是function composition 运算符。定义如下:

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。他们更喜欢将countWhere 写成pointfree 风格,如下所示:

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:

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

同样我们得到:

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

如您所见,(.:)(.::)(.:::) 只是(.) 的幂(即它们是(.)iterated functions)。对于数学中的数字:

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,这非常简洁。

【讨论】:

  • (.) ^ i 形式的表达式不是很好的类型,因此它们实际上不是有效的 Haskell。
  • 是的。但是我确实写了 "Similarly for functions in Mathematics:" 并且由于这是一个数学解释,我认为可以将 ^ 用于函数而不是数字。不过,我会将运算符更改为 .^ 以区分两者。
  • +1 -- 不错且清晰的答案。但是,我完全不同意无点在任何方面都更具可读性或更好。 countWhere f xs = length (filter f xs) 比所有其他版本都更具可读性,因为它立即清楚 1)函数有多少参数,2)它接受什么样的参数(通过“智能”名称)3)几乎任何程序员都可以阅读并在几秒钟内对正在发生的事情有一些直觉。找出无点版本需要更多时间,尤其是当表达式变得比简单组合更复杂时。
  • @Bakuriu 我从来没有提到无点风格在任何方面都更具可读性或更好。我唯一一次提到“可读”这个词是与 tac 示例结合使用的。我同意,人们会被无点编程迷住。以无点风格编写的mf 函数就是其中的典范。然而,从数学角度研究无点编程风格很有趣,因为它使程序的结构在数学上更易于处理。它可以使您的代码更短,但容量有限。
  • 在使用了 5 年 Haskell 之后,由于阅读了这个答案,我终于摸不着头脑了。
【解决方案2】:

这是一个品味问题,但我觉得这种风格令人不快。首先我会描述它的含义,然后我会提出一个我更喜欢的替代方案。

您需要知道(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)
  • @AaditMShah 在这种情况下,我更喜欢<$$> = fmap . fmap 这样的东西,因为(.:) 按照惯例专门用于(->) r,而“外部”fmap(->) r反正函子。