瞭解 JavaScript 函數語言程式設計-型別簽名

Pandaaa發表於2019-05-06

目錄

型別簽名

初識型別

JavaScript 是一種動態的型別語言,但這並不意味著要否定型別的使用。我們日常打交道的主要就是字串、數值、布林值等。雖然 JavaScript 語言成面上沒有相關的整合。不過我們可以使用型別簽名生成文件,也可以使用註釋幫助我們區分型別。

有些朋友應該使用過一些 JavaScript 型別檢查工具,比如 Flow 或者 是其他的靜態型別檢測語言類如 TypeScript。

Hindley-Milner 型別簽名

型別簽名是一個非常常用的系統,我們可以從很多計算機語言系統上看到它的使用,下面來看個栗子:

//  capitalize :: String -> String
var capitalize = function(s){
  return toUpperCase(head(s)) + toLowerCase(tail(s));
}

capitalize("smurf");
//=> "Smurf"
複製程式碼

這裡的 capitalize 接受一個 String 並返回了一個 String。這裡我們不關心實現函式過程,我們只關注它的型別簽名

Hindley-Milner 系統中,函式都寫成類似 a -> b 這個樣子,其中 ab 是任意型別的變數。因此,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);
});

複製程式碼

strLengthcapitalize 類似:接受一個 String 然後返回一個 Number

具體來看看 match 函式

對於 match 函式,我們完全可以把它的型別簽名這樣分組:

//  match :: Regex -> (String -> [String])
var match = curry(function(reg, s){
  return s.match(reg);
});
複製程式碼

是的,把最後兩個型別包在括號裡就能反映更多的資訊了。現在我們可以看出 match 這個函式接受一個 Regex 作為引數,返回一個從 String[String] 的函式。因為curry,造成的結果就是這樣:給 match 函式一個 Regex,得到一個新函式,能夠處理其 String 引數。當然了,我們並非一定要這麼看待這個過程,但這樣思考有助於理解為何最後一個型別是返回值。

//  match :: Regex -> (String -> [String])

//  onHoliday :: String -> [String]
var onHoliday = match(/holiday/ig);
複製程式碼

每傳一個引數,就會彈出型別簽名最前面的那個型別。所以 onHoliday 就是已經有了 Regex 引數的 match

//  replace :: Regex -> (String -> (String -> String))
var replace = curry(function(reg, sub, s){
  return s.replace(reg, sub);
});
複製程式碼

但是在這段程式碼中,就像你看到的那樣,為 replace 加上這麼多括號未免有些多餘。所以這裡的括號是完全可以省略的,如果我們願意,可以一次性把所有的引數都傳進來;所以,一種更簡單的思路是:replace 接受三個引數,分別是 RegexString 和另一個 String,返回的還是一個 String

如果你使用過 TypeScript 來看看下面的改寫

//  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 的時候更加的方便。

縮小可能性範圍 narrowing of possibility

一旦引入一個型別變數,就會出現一個奇怪的特性叫做 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

型別簽名除了能夠幫助我們推斷函式可能的實現,還能夠給我們帶來自由定理(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,用了就回不去的好玩物。

相關文章