减少繁琐的括号嵌套
Haskell中的求值顺序在1介绍。为了明确表示求值顺序,计算表达式中通常需要复杂的括号嵌套,比如((1+2)+(3*4)/(5/6))*(7+(8+9)-10)
。但是,繁琐的括号嵌套有如下缺点:
- 会引起视觉负担,在括号较为繁琐时并不能方便地看出表达式的求值顺序。
- 繁琐的括号嵌套也和数学直觉相悖。在数学中,
sin cos 1
代表的就是sin(cos(1))
的计算顺序。
我们需要减少程序中的括号嵌套,增加计算式的可读性。有两种方法,用一个式子表示:f $ g $ h x = (f . g . h) x = f (g (h x))
,也就是使用函数应用符和函数复合符都可以减少括号的使用。但是这两种东西的本质完全不同:分别介绍在下面。
函数应用符:($) and (&)
infixr 0 $
($) :: (a -> b) -> a -> b
f $ x = f x
考虑到$
的最低优先级和右结合性,完全可以把$
视为表达式的分割符:表达式被若干个$
分割成若干块piece1 $ .. $ piecen
,在求值的时候首先求piecen
,然后继续求值piece(n-1)
,在此期间apply掉最右侧的$
,以此类推,求整个式子的值。
举个例子:f (g1 h1 x) (g2 (h2 x y) z)
- 用
$
初步分块成f (g1 h1 x) $ g2 (h2 x y) z
,表示先求g2 (h2 x y) z
再求f (g1 h1 x) ..
; - 然后继续再不破坏计算顺序的情况下把括号变成
$
,比如f (g1 h1 x) $ (g2 $ h2 x y) z
等等。
在用$
化简计算式时,
infixl 1 &
(&) :: a -> (a -> b) -> b
x & f = f x
&
视为对$
的一个补充,在使用方面的习惯和$
是完全一样的,只是&
调换了两个参数的顺序。
既然如此,为什么要引入&
呢?&
用来在表达式中连续使用,来对参数连续应用函数:x & f1 & .. & fn = fn (.. (f1 x))
. 总体来说,$
的使用是为了减少括号嵌套,而&
的使用场景是对标>>=
的,方便对参数连续应用函数。应用两者的目的都是在编程时减少视觉负担,所以两者的引入都是必要的。
函数复合符:(.)
infixr 9 .
(.) :: (b -> c) -> (a -> b) -> a -> c
g . f = \x -> g (f x)
我们在使用(.)
的时候,只将其视为一个普通的中缀函数。因为其优先级最高,仅低于函数调用,所以在出现时,将其认为是组合左边已经求值的部分的中缀函数,功能是transform那部分值的类型,就可以理解。
(这样说只是方便理解!实际上不是这样的,.
作为运算符,应用会比函数晚)。
比如(f . g . h) x
中,假设f :: c -> d, g :: b -> c, h :: a -> b
。由于右结合性,首先应用第二个.
,应用在g
和h
上得到g . h :: a -> c
。然后应用第一个.
,得到f . (g . h) :: a -> d
,最后应用在x
上,得到f . (g . h) x :: d
.
不难发现写成f . g . h x
是错误的,因为h x
会先求值,导致第二个.
的类型错误。