“.”(点号)和“$”(美元符号)有什么区别?

802

点号(.)和美元符号($)有什么区别?

据我所知,它们都是语法糖,用于不需要使用括号。

14个回答

1361

$操作符用于避免括号。它之后的任何内容都会比之前的内容优先执行。

例如,假设你有一行代码:

putStrLn (show (1 + 1))

如果你想要去掉这些括号,下面任何一行代码都能实现同样的效果:

putStrLn (show $ 1 + 1)
putStrLn $ show (1 + 1)
putStrLn $ show $ 1 + 1
.运算符的主要目的不是为了避免使用括号,而是为了链式调用函数。它允许你将右侧表达式的输出与左侧表达式的输入相连接。这通常也会导致更少的括号,但是工作方式不同。

回到同一个例子:

putStrLn (show (1 + 1))
  1. (1 + 1) 没有输入,因此不能与 . 运算符一起使用。
  2. show 可以接受一个 Int 并返回一个 String
  3. putStrLn 可以接受一个 String 并返回一个 IO ()

您可以像这样将 show 链接到 putStrLn

(putStrLn . show) (1 + 1)

如果您对那么多括号感到不满意,可以使用$运算符来摆脱它们:

putStrLn . show $ 1 + 1

74
实际上,由于"+"也是一个函数,你是否可以将其前缀化,然后将其组合起来使用,比如 putStrLn . show . (+) 1 1?虽然这并没有更加清晰,但我的意思是......你可以这样做,对吗? - CodexArcanum
9
еңЁиҝҷдёӘдҫӢеӯҗдёӯпјҢзұ»дјјдәҺ putStrLn . show . (+1) $ 1 зҡ„иЎЁиҫҫејҸжҳҜзӯүд»·зҡ„гҖӮдҪ иҜҙеҫ—еҜ№пјҢеӨ§еӨҡж•°пјҲжүҖжңүпјҹпјүдёӯзјҖиҝҗз®—з¬ҰйғҪжҳҜеҮҪж•°гҖӮ - Michael Steele
88
我想知道为什么没有人提到像 map ($3) 这样的用法。我的意思是,我大多数时候使用 $ 是为了避免使用括号,但这并不意味着它们只有这个作用。 - Cubic
54
map ($3) 是一个类型为 Num a => [(a->b)] -> [b] 的函数。它接受一个函数列表,这些函数都需要一个数字作为参数,并将数字 3 应用到它们所有的函数上,然后收集结果。 - Cubic
28
在使用$符号与其他运算符一起时,你需要小心。"x + f(y+z)"并不等同于"x + f $ y + z",因为后者实际上意味着"(x+f)(y+z)"(即将x和f的和视为一个函数)。 - Paul Johnson
显示剩余8条评论

215

它们有不同的类型和不同的定义:

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

infixr 0 $
($) :: (a -> b) -> a -> b
f $ x = f x

($)被设计用来替代普通的函数应用,但是它具有不同的优先级,以帮助避免使用括号。 (.)用于将两个函数组合在一起,以创建一个新函数。

在某些情况下,它们是可以互换的,但这并不总是正确的。它们通常可以互换的典型例子是:

f $ g $ h $ x

==>

f . g . h $ x

换句话说,在一串 $ 中,除了最后一个以外的所有字符都可以被替换为 .

3
如果 x 是一个函数,那么你能把 . 作为最后一个吗? - richizy
4
如果在这个情境中你真的应用了"x",那么是的——但是这时的“最终”的结果将应用到除了“x”以外的某个事物上。如果你没有应用“x”,那么它就跟“x”是一个值没有什么不同。 - GS - Apologise to Monica

136

还要注意 ($) 是专门针对函数类型的恒等函数(identity function)。恒等函数长这样:

id :: a -> a
id x = x

虽然 ($) 看起来像这样:

($) :: (a -> b) -> (a -> b)
($) = id
请注意,我在类型签名中有意添加了额外的括号。 使用`($)`通常可以通过添加括号来消除(除非运算符在部分中使用)。例如:`f $ g x`变成`f (g x)`。 使用`(.)`通常更难替换;它们通常需要lambda或引入显式函数参数。例如:
f = g . h

变成

f x = (g . h) x

变成

f x = g (h x)

请注意,我在类型签名中故意添加了额外的括号。我有点困惑...你为什么要这样做? - Mateen Ulhaq
10
($) 的类型是 (a -> b) -> a -> b,与 (a -> b) -> (a -> b) 相同,但加上额外的括号可以增加一些清晰度。 - Rudi
4
哦,我明白了。我一开始认为这是一个带有两个参数的函数……但是由于柯里化,它等价于返回另一个函数的函数。 - Mateen Ulhaq

84

($)允许函数在不添加括号控制求值顺序的情况下链接在一起:

Prelude> head (tail "asdf")
's'

Prelude> head $ tail "asdf"
's'

组合运算符(.)创建一个新的函数,而不指定参数:

Prelude> let second x = head $ tail x
Prelude> second "asdf"
's'

Prelude> let second = head . tail
Prelude> second "asdf"
's'

上面的例子可以说是很有说明性,但并不能真正展现使用组合的便利性。这里提供另一个比喻:

Prelude> let third x = head $ tail $ tail x
Prelude> map third ["asdf", "qwer", "1234"]
"de3"

如果我们仅使用third一次,我们可以通过使用lambda避免对其命名:

Prelude> map (\x -> head $ tail $ tail x) ["asdf", "qwer", "1234"]
"de3"

最后,组合让我们避免使用lambda:

Prelude> map (head . tail . tail) ["asdf", "qwer", "1234"]
"de3"

7
如果stackoverflow有一个组合函数,我会更喜欢结合前两个解释和这个回答中的例子来得出答案。 - Chris.Q

62

简短而精炼的版本:

  • ($) 调用其左侧参数为函数,右侧参数为值的函数。
  • (.) 将其左侧参数为函数和右侧参数为函数组合起来。

36

Learn You a Haskell的非常简短的描述中,有一个实用但让我花了一些时间才弄明白的应用程序:自从……

f $ x = f x

当包含中缀运算符的表达式的右侧用括号括起来时,它就变成了前缀函数。因此可以写出($ 3) (4 +),类似于(++ ", world") "hello"

为什么会这样做?比如说,用于函数列表。

map (++ ", world") ["hello", "goodbye"]
map ($ 3) [(4 +), (3 *)]

短于

map (\x -> x ++ ", world") ["hello", "goodbye"]
map (\f -> f 3) [(4 +), (3 *)]

显然,后一种变体对大多数人来说更易读。

16
顺便提一句,我建议不要使用 $3 这样没有空格的写法。如果启用了 Template Haskell,这将被解释为语法插入,而 $ 3 始终表示你所说的意思。总的来说,在 Haskell 中有一种趋势是通过坚持某些运算符周围必须有空格来“窃取”语法位。 - GS - Apologise to Monica
1
我花了一段时间才弄清楚括号是如何工作的:http://en.wikibooks.org/wiki/Haskell/More_on_functions#Infix_versus_Prefix - Casebash
通常在像这样的好问题上,我们会看到一个非常好的答案。然而,我们有多个极好的答案,每个答案都进一步阐明了观点,并提供了另一个有助于理解的方面。太棒了! - Jay Imerman

33

Haskell:点号(.)和美元符号($)之间的区别

点号(.)和美元符号($)的区别是什么?据我所知,它们都是语法糖,用于不需要使用括号。

它们不是用于不需要使用括号的语法糖-它们是函数,中缀的,因此我们可以称它们为运算符。

组合、(.)以及何时使用它

(.)是组合函数。

result = (f . g) x

构建一个函数,该函数将其参数传递给g的结果传递到f中,与构建将其参数传递给f的函数相同。

h = \x -> f (g x)
result = h x

当您没有可用的参数来传递给要组合的函数时,请使用(.)

右结合的应用($)及其使用时机

($)是一个具有低绑定优先级的右结合应用函数。因此,它仅首先计算其右侧的内容。

result = f $ g x

从程序上来看,这与以下代码是等价的(这很重要,因为Haskell被惰性地求值,它将开始首先评估f):

h = f
g_x = g x
result = h g_x
更简洁地说:
result = f (g x)

当你在应用前一个函数的结果之前,已经有了所有要计算的变量时,请使用($)

我们可以通过阅读每个函数的源代码来理解这一点。

阅读源代码

这里是(.)源代码

-- | Function composition.
{-# INLINE (.) #-}
-- Make sure it has TWO args only on the left, so that it inlines
-- when applied to two functions, even if there is no final argument
(.)    :: (b -> c) -> (a -> b) -> a -> c
(.) f g = \x -> f (g x)

这是($)源代码

-- | Application operator.  This operator is redundant, since ordinary
-- application @(f x)@ means the same as @(f '$' x)@. However, '$' has
-- low, right-associative binding precedence, so it sometimes allows
-- parentheses to be omitted; for example:
--
-- >     f $ g $ h x  =  f (g (h x))
--
-- It is also useful in higher-order situations, such as @'map' ('$' 0) xs@,
-- or @'Data.List.zipWith' ('$') fs xs@.
{-# INLINE ($) #-}
($)                     :: (a -> b) -> a -> b
f $ x                   =  f x

结论

当您不需要立即求值函数时,请使用组合。也许您想将组合的结果函数传递给另一个函数。

当您提供所有参数进行完全求值时,请使用应用。

因此,对于我们的示例,最好语义上这样做:

f $ g x

当我们有x(或者更确切地说是g的参数)时,执行以下操作:

f . g

当我们不这样做时。


1
在所有出色的答案中,我认为这个或许应该被“优先”阅读——它提供了最准确和最易理解的解释。然而,其他答案仍然提供了更多信息。 - Jay Imerman

13

...或者您可以通过使用管道技术避免使用 . $ 结构:

third xs = xs |> tail |> tail |> head

在添加了辅助函数之后:

(|>) x y = y x

2
是的,|> 是 F# 管道运算符。 - user1721780
6
这里需要注意的一点是,Haskell 的 $ 运算符实际上更像 F# 的 <| 而不是 |>。通常在 Haskell 中,你会像这样编写上面的函数:third xs = head $ tail $ tail $ xs 或者甚至像这样:third = head . tail . tail,在 F# 风格的语法中,它会变成这样:let third = List.head << List.tail << List.tail - Electric Coffee
1
为什么要添加一个辅助函数,让 Haskell 看起来像 F#?-1 - vikingsteve
11
反转的 $ 已经存在,它被称为 &。具体信息请查看此链接:https://hackage.haskell.org/package/base-4.8.0.0/docs/Data-Function.html#v:-38-。 - pat

13

我的规则很简单(我也是新手):

  • 如果要传递参数(调用函数),则不要使用
  • 如果还没有参数(组成一个函数),则不要使用$

就是这样。

show $ head [1, 2]

但永远不要:

show . head [1, 2]

6
好的启发式方法,但需要更多的例子。 - Zoey Hewll

12

学习任何事情(任何函数)的好方法是记住一切都是函数!这个普遍的口号有所帮助,但在特定情况下,如操作符,记住这个小技巧会更加有用:

:t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c

并且

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

记得要广泛使用:t,并将运算符包裹在()中!


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