Haskell:理解函数应用符和函数复合符,并应用($), (&), (.)减少嵌套调用中的括号嵌套

减少繁琐的括号嵌套

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。由于右结合性,首先应用第二个.,应用在gh上得到g . h :: a -> c。然后应用第一个.,得到f . (g . h) :: a -> d,最后应用在x上,得到f . (g . h) x :: d.

不难发现写成f . g . h x是错误的,因为h x会先求值,导致第二个.的类型错误。

参考


  1. Haskell: 表达式的计算顺序 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章