JavaScript 是一種動態的類型語言,但這並不意味着要否認類型的使用。咱們平常打交道的主要就是字符串、數值、布爾值等。雖然 JavaScript 語言成面上沒有相關的集成。不過咱們可使用類型簽名生成文檔,也可使用註釋幫助咱們區分類型。編程
有些朋友應該使用過一些 JavaScript 類型檢查工具,好比 Flow 或者 是其餘的靜態類型檢測語言類如 TypeScript。api
類型簽名是一個很是經常使用的系統,咱們能夠從不少計算機語言系統上看到它的使用,下面來看個栗子:數組
// capitalize :: String -> String
var capitalize = function(s){
return toUpperCase(head(s)) + toLowerCase(tail(s));
}
capitalize("smurf");
//=> "Smurf"
複製代碼
這裏的
capitalize
接受一個String
並返回了一個String
。這裏咱們不關心實現函數過程,咱們只關注它的類型簽名bash
在 Hindley-Milner
系統中,函數都寫成相似 a -> b
這個樣子,其中 a
和 b
是任意類型的變量。所以,capitalize
函數的類型簽名能夠理解爲「一個接受 String
返回 String
的函數」。換句話說,它接受一個 String
類型做爲輸入,並返回一個 String
類型的輸出。函數式編程
// strLength :: String -> Number
var strLength = function(s){
return s.length;
}
// join :: String -> [String] -> String
var join = curry(function(what, xs){
return xs.join(what);
});
// match :: Regex -> String -> [String]
var match = curry(function(reg, s){
return s.match(reg);
});
// replace :: Regex -> String -> String -> String
var replace = curry(function(reg, sub, s){
return s.replace(reg, sub);
});
複製代碼
strLength
和 capitalize
相似:接受一個 String
而後返回一個 Number
。函數
對於 match 函數,咱們徹底能夠把它的類型簽名這樣分組:工具
// match :: Regex -> (String -> [String])
var match = curry(function(reg, s){
return s.match(reg);
});
複製代碼
是的,把最後兩個類型包在括號裏就能反映更多的信息了。如今咱們能夠看出 match
這個函數接受一個 Regex
做爲參數,返回一個從 String
到 [String]
的函數。由於curry
,形成的結果就是這樣:給 match
函數一個 Regex
,獲得一個新函數,可以處理其 String
參數。固然了,咱們並不是必定要這麼看待這個過程,但這樣思考有助於理解爲什麼最後一個類型是返回值。post
// match :: Regex -> (String -> [String])
// onHoliday :: String -> [String]
var onHoliday = match(/holiday/ig);
複製代碼
每傳一個參數,就會彈出類型簽名最前面的那個類型。因此 onHoliday
就是已經有了 Regex
參數的 match
。ui
// replace :: Regex -> (String -> (String -> String))
var replace = curry(function(reg, sub, s){
return s.replace(reg, sub);
});
複製代碼
可是在這段代碼中,就像你看到的那樣,爲 replace
加上這麼多括號未免有些多餘。因此這裏的括號是徹底能夠省略的,若是咱們願意,能夠一次性把全部的參數都傳進來;因此,一種更簡單的思路是:replace
接受三個參數,分別是 Regex
、String
和另外一個 String
,返回的仍是一個 String
。搜索引擎
// capitalize :: String -> String
let capitalize = (s: String): String => {
toUpperCase(head(s)) + toLowerCase(tail(s));
}
// match :: Regex -> (String -> [String])
let match = curry((reg:RegExp, s:String): string[] =>{
s.match(reg);
});
複製代碼
能夠看到 TypeScript 的語法更加易於理解不須要註釋你們應該也能明白輸入和輸出的類型,咱們能夠知道 TypeScript 是借鑑類相似於類型簽名的思想去作的類型檢測,以致於咱們使用 JavaScript 的時候更加的方便。
一旦引入一個類型變量,就會出現一個奇怪的特性叫作 parametricity(en.wikipedia.org/wiki/Parame… )。這個特性代表,函數將會以一種統一的行爲做用於全部的類型。咱們來研究下:
// head :: [a] -> a
複製代碼
注意看 head
,能夠看到它接受 [a]
返回a
。咱們除了知道參數是個數組,其餘的一律不知;因此函數的功能就只限於操做這個數組上。在它對 a
一無所知的狀況下,它可能對 a
作什麼操做呢?換句話說,a
告訴咱們它不是一個特定的類型,這意味着它能夠是任意類型;那麼咱們的函數對每個可能的類型的操做都必須保持統一。這就是 parametricity
的含義。要讓咱們來猜想 head
的實現的話,惟一合理的推斷就是它返回數組的第一個,或者最後一個,或者某個隨機的元素;固然,head
這個命名應該能給咱們一些線索。 再看一個例子:
// reverse :: [a] -> [a]
複製代碼
僅從類型簽名來看,reverse
可能的目的是什麼?再次強調,它不能對 a
作任何特定的事情。它不能把 a
變成另外一個類型,或者引入一個 b
;這都是不可能的。那它能夠排序麼?答案是不能,沒有足夠的信息讓它去爲每個可能的類型排序。它能從新排列麼?能夠的,我以爲它能夠,但它必須以一種可預料的方式達成目標。另外,它也有可能刪除或者重複某一個元素。重點是,無論在哪一種狀況下,類型 a
的多態性(polymorphism)都會大幅縮小 reverse
函數可能的行爲的範圍。
這種「可能性範圍的縮小」(narrowing of possibility)容許咱們利用相似 Hoogle
這樣的類型簽名搜索引擎去搜索咱們想要的函數。類型簽名所能包含的信息量真的很是大。
類型簽名除了可以幫助咱們推斷函數可能的實現,還可以給咱們帶來自由定理(free theorems)。來看一個栗子
// head :: [a] -> a
compose(f, head) == compose(head, map(f));
複製代碼
例子中,等式左邊說的是,先獲取數組的第一個元素,而後對它調用函數 f;等式右邊說的是,先對數組中的每個元素調用 f,而後再取其返回結果的頭部。這兩個表達式的做用是相等的,可是前者要快得多。
在 JavaScript 中,你能夠藉助一些工具來聲明重寫規則,也能夠直接使用 compose 函數來定義重寫規則。總之,這麼作的好處是顯而易見且唾手可得的,可能性則是無限的。若是這裏不太明白 compose 的使用的話,能夠翻到前面看看 code compose 的文章解釋代碼組合的優點
最後要注意的一點是,簽名也能夠把類型約束爲一個特定的接口(interface)。
// sort :: Ord a => [a] -> [a]
複製代碼
雙箭頭左邊代表的是這樣一個事實:a 必定是個 Ord 對象。也就是說,a 必需要實現 Ord 接口。Ord 究竟是什麼?它是從哪來的?在一門強類型語言中,它可能就是一個自定義的接口,可以讓不一樣的值排序。經過這種方式,咱們不只可以獲取關於 a 的更多信息,瞭解 sort 函數具體要幹什麼,並且還能限制函數的做用範圍。咱們把這種接口聲明叫作類型約束(type constraints)。
// assertEqual :: (Eq a, Show a) => a -> a -> Assertion
複製代碼
這個例子中有兩個約束:Eq 和 Show。它們保證了咱們能夠檢查不一樣的 a 是否相等,並在有不相等的狀況下打印出其中的差別。 咱們將會在後面的章節中看到更多類型約束的例子,其含義也會更加清晰。
Hindley-Milner
類型簽名在函數式編程中無處不在,它們簡單易讀,寫起來也不復雜。但僅僅憑簽名就能理解整個程序仍是有必定難度的,要想精通這個技能就更須要花點時間了。固然如今是推薦你們使用 TypeScript
,用了就回不去的好玩物。