$
符号并不是用于替换括号的魔法语法。它是一个普通的中缀运算符,就像
+
运算符一样。将括号放在单个名称周围,例如
(xs)
,永远等同于只使用
xs
1。因此,如果
$
这样做,那么你无论哪种方式都会得到相同的错误。试想一下,如果你在那里使用了其他熟悉的运算符,比如
+
,会发生什么:
let findKey key xs = snd . head . filter (\(k,v) -> key == k) + xs
忽略+
在数字上的作用,这没有意义,只需考虑表达式的结构; 哪些术语被识别为函数,哪些术语被作为参数传递给它们。
实际上,在那里使用+
确实可以成功解析和类型检查!(它会给你一个带有无意义类型类约束的函数,但是如果您满足它们,它确实有意义)。 让我们一起走过中缀运算符的解析过程:
let findKey key xs = snd . head . filter (\(k,v) -> key == k) + xs
最高优先级的操作始终是普通函数应用(仅在彼此之间编写术语,不涉及中缀运算符)。这里只有一个例子,即 filter
应用于 lambda 定义。它被"解析"后就成为一个单独的子表达式,从而在解析其余运算符时得到考虑:
let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
in snd . head . fileterExp + xs
下一个最高优先级的运算符是“.”操作符。我们有几个选择,在这里它们的优先级是相同的。“.”是右结合的,所以我们首先取最右边的一个(但无论我们选哪一个,实际上都不会改变结果,因为“.”的意义是关联操作,但解析器无法知道这一点)。
let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
dotExp1 = head . filterExp
in snd . dotExp1 + xs
请注意,
.
会立即抓取其左右的术语。这就是为什么优先级如此重要的原因。还剩下一个
.
,它的优先级仍高于
+
,所以接下来处理它:
let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
dotExp1 = head . filterExp
dotExp2 = snd . dotExp1
in dotExp2 + xs
我们完成了!在这里,
+
的优先级最低,因此它最后获得其参数,并成为整个表达式中最高层的调用。请注意,
+
的低优先级防止了
xs
被左侧任何更高优先级的应用程序声明为参数。如果它们中的任何一个优先级较低,它们将以整个表达式
dotExp2 + xs
作为参数,因此它们仍然无法到达
xs
;在
xs
之前放置中缀运算符(
任何中缀运算符)可防止其被左侧任何东西声明为参数。
实际上,这正是在该表达式中解析
$
的方式,因为
.
和
$
恰好具有与
.
和
+
相同的相对优先级;
$
的设计具有极低的优先级,因此它将以几乎任何其他涉及左右的操作符的方式工作。
如果我们在
filter
调用和
xs
之间
不放置中缀运算符,则会发生以下情况:
let findKey key xs = snd . head . filter (\(k,v) -> key == k) xs
首先进行普通函数应用。这里我们有三个术语简单地并列在一起:filter
,(\(k,v) -> key == k)
和xs
。函数应用是左结合的,因此我们首先取最左侧的一对:
let findKey key xs
= let filterExp1 = filter (\(k,v) -> key == k)
in snd . head . filterExp1 xs
还有另一个常见的应用程序,它的优先级仍然高于.
,所以我们执行它:
let findKey key xs
= let filterExp1 = filter (\(k,v) -> key == k)
filterExp2 = filterExp1 xs
in snd . head . filterExp2
现在是第一个点:
let findKey key xs
= let filterExp1 = filter (\(k,v) -> key == k)
filterExp2 = filterExp1 xs
dotExp = head . filterExp2
in snd . dotExp
我们完成了,这次整个表达式中最顶部的调用是最左边的
.
运算符。这次
xs
作为第二个参数被吸入
filter
。这有点像我们想要的,因为
filter
确实需要两个参数,但它将
filter
的结果留在函数组合链中,而对两个参数应用的
filter
不能返回一个函数。我们想要的是将其应用于一个参数,生成一个函数,使该函数成为函数组合链的一部分,然后将整个函数应用于
xs
。
有了
$
,最终形式与我们使用
+
时相同:
let findKey key xs
= let filterExp = filter (\(k,v) -> key == k)
dotExp1 = head . filterExp
dotExp2 = snd . dotExp1
in dotExp2 $ xs
它的解析方式与我们使用 '+' 时完全相同,所以唯一的区别在于,当 '+' 表示“将我的左参数添加到我的右参数”时,“$” 表示“将我的左参数作为函数应用于我的右参数”。这正是我们想要的!太好了!
简而言之:坏消息是 '$' 并不能仅仅通过括号包裹来工作;它比那更复杂。好消息是,如果你理解 Haskell 解析涉及中缀运算符的表达式的方式,那么你就理解了 '$' 的工作原理。对于语言本身来说,它并没有什么特殊性;它就是一个普通的操作符,如果它不存在,你也可以自己定义它。
1. 像 '+' 这样的操作符加上括号只是给你同样由 '+' 表示的函数,但现在它不再有特殊的中缀语法,因此在这种情况下会影响到如何解析东西。但像 '(xs)' 这样的名称内部则不是这样。