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