Haskell小白之 區分 ($) 和 (.) 兩個函數

每次看 Haskell 代碼,看到一堆 ($) 和 (.) 混雜在一起,老是讓人一會兒摸不着頭腦。像我這樣的半吊子選手,常常一段時間不看,這兩位就又分不清了。算了,此次就把他倆的區別寫一寫,理理清楚,完全搞明白這倆大兄弟。數組

按照了解一個 Haskell 中函數的基本套路,先去分析他們的類型聲明:函數

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

是否是一目瞭然?code

($)

從類型聲明中能夠看出,($) 函數接受兩個參數 —— (a -> b) 和 a ,而這個函數的返回值與其第一個參數的返回值相同,(它的第一個參數自己就是函數)。舉個例子:ip

f $ x

這個例子其實等同於 f x,這不就是至關於直接對 (a -> b) 這個函數直接應用 a 這個輸入麼。那麼 ($) 自己存在的意義是什麼呢?數學

($) 這個符號,在Haskell中叫作函數應用符,它主要被用來下降函數的優先級,即這個函數具備右結合性,這又怎麼理解呢?it

f a b c
-- 這個函數在Haskell中等同於
(((f a) b) c)
-- 可是若是你的需求是這樣的呢
f a (b c)
-- 即先計算最右側的表達式的值,那咱們就能夠用 $ 來代替括號了
f a $ b c

因此 ($) 函數在Haskell中的主要用途就是來減小括號的使用的,讓代碼看上去更簡潔。再舉個實際的例子:class

3 * (1 + 2)
-- 換成用 $ 表達 能夠寫成
(*) 3 $ 1 + 2

固然這個例子還不太可以展現 $ 的優點,咱們再舉個例子(前方高能):基礎

sum (filter (>10) (map (*2) [2..10]))
-- 將2-10這9個數字分別乘2獲得一個新數組,而後過濾掉數組中大於10的數,將剩餘的
-- 數字求和,最後結果爲80
-- 使用 $ 後,變成了這樣
sum $ filter (>10) $ map (*2) [2..10]

sum (map sqrt [1..130])
-- 等價於
sum $ map sqrt [1..130]

length (takeWhile (<1000) (scanl1 (+) (map sqrt [1..]))) + 1
-- 等價於
(+1) $ length $ takeWhile (<1000) $ scanl1 (+) $ map sqrt [1..]

(.)

從類型聲明中能夠看出來,(.) 函數實際上將 (b -> c) . (a -> b) 從新糅合成一個新的函數,即 (a -> c)。想必你也能猜到了,(.)的做用就是組合函數,將兩個函數組合成一個新的函數,第二個參數的輸出,是第一個參數的輸入,而且新生成的這個函數,以第二個參數的輸入做爲輸入,以第一個參數的輸出做爲輸出。用數學來表示,就是這樣的 (f · g)(x) = f(g(x))lambda

-- 數學表述以下:
-- f(x) = x + 1
-- g(x) = 2x
-- o(x) = f(g(x))
-- 如今咱們要的到o這樣一個函數,使用函數組合就是下面這個樣子
(+1).(*2)
-- 等價於
(\x -> x * 2) + 1

能夠看出使用 (.) 函數,能夠很方便的進行函數組合,而無需再出現多餘的 lambda 表達式。map

(.) 和 ($) 配合起來使用,能夠省去不少括號,讓代碼看起來十分簡潔(前方高能):

-- 不使用的寫法
replicate 2 (product (map (*3) (zipWith max [1, 2] [4, 5])))
-- 使用以後
replicate 2 . product . map (*3) $ zipWith max [1, 2] [4, 5]
-- 還能夠這樣
replicate 2 . product . map (*3) . zipWith max [1, 2] $ [4, 5]

是否是簡潔了不少。

總結

通過剛剛的梳理,能夠看到二者有本質上的不一樣,但因爲 ($) 和 (.) 常常會同時出如今代碼中,若是基礎不夠紮實,仍是會很容易把二者搞混。爲了區分他們,只須要時刻謹記 —— (.) 是用來作函數組合的,($) 是用來下降函數執行優先級的。

相關文章
相關標籤/搜索