关于 haskell 的.和$运算符

函数应用是左结合的且优先级最高,然后是.运算符,最后是$运算符。

所以对表达式——

1
sum . map product $ [[6, 7]]

首先是 map 进行调用,其以 product 为参数——

1
sum . (map product) $ [[6, 7]]

这里,假设$先调用,则会变成这样的结果——

1
2
sum . ((map product) [[6, 7]])
--- 等价于 sum . [42]

这显然是离谱的,因此,.将首先调用,形成这样的结果——

1
2
sum ((map product) $ [[6, 7]])
--- 等价于 sum [42]

Bingo!

在这里,$运算符的意义在于,避免最靠近参数的函数直接计算出了结果,而是延迟到计算结果的前一刻(也就是得到以输入参数为唯一参数的函数),待.运算符将各个函数组合后再进行真正的运算。

可以认为,$把原表达式变成这样了——

1
(sum . map product) [[6, 7]]

这或许是对该运算符的最容易理解的诠释。

用 kotlin 的话来说,它代表这样的链式调用——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 可大剌剌地直接理解为——
{{6, 7}} // 应当是 List.of,这里只是为了清晰
.let (v -> map(product, v))
.let (v -> sum(v));

// 柯里化的形式,复杂但更加接近实质
{{6, 7}}
.let (v -> {
val tmpFn = (v1) -> {
map(product, v1)
}
tmpFn(v)
}).let (v -> {
val tmpFn = (v1) -> { // 可以直接 val tmpFn = sum
sum(v1)
}
tmpFn(v)
});

如果最近的函数的入参有多个怎么办?考虑$的签名:($) :: (a -> b) -> a -> b,其左边应该是函数,右边应该是参数,事情变得明了起来了——将$加到最后一个参数前面,比如下面的示例,它定义了一个获取坐标到原点距离的函数——

1
2
3
distance :: Floating c => c -> c -> c
distance x y = sqrt . squareSum x $ y
where squareSum x y = x ^ 2 + y ^ 2

我觉得这在代码结构上实在不太优雅,但或许之后会有改变。转换成 Kotlin,对distance 3 4,即sqrt . squareSum 3 $ 4,有这样的等价代码——

1
2
3
4
5
6
7
8
9
10
11
4.let(v -> {
val tmpFn = (v1) -> {
squareSum(3, v1)
}
tmpFn(v)
})).let(v -> {
val tmpFn = (v1) -> {
sqrt (v1)
}
tmpFn(v)
});

对参数是表达式的情况,也可以使用$来减少括号——

1
2
fib (3 + 4 + 5)
--- 等价于 fib $ 3 + 4 + 5

因为$最低的优先级,3+4+5将被先计算。当然,这特性可无法用在fib (n - 1) + fib (n - 2)中。

顺带一提,.运算符的签名如下,可见其组合的函数必须是接受单参数的,且最终组合的函数,接受参数为最内的函数的参数的类型,返回结果为最外的函数的返回值的类型。

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