Why Type Systems Matter

Matthias Endler

為什麼型別系統很重要

我大部分的程式碼都是用 Python 或 PHP 這類動態型別語言撰寫的;但自從接觸 Rust 之後,我便對靜態型別系統產生了熱情。對我來說,這開始變得非常自然;就像一種全新的自我表達方式。

型別是來幫忙的

透過型別,你可以傳達你的保證與期望。無論是對機器,還是對其他開發者。型別表達的是意圖。

身為程式設計師,你可能已經對型別培養出一些直覺

sentence = "hello world"

你可能會猜 sentence 是字串。畢竟它被引號包起來了。如果型別是從其他地方推斷而來,情況就會變得有點棘手。

sentence = x

sentence 還是字串嗎?呃……我們不知道。這取決於 x 的型別。也許 x 是數字,所以 sentence 也是數字?也許 x 曾經是字串,但在重構之後現在變成了位元組陣列?大家都玩得很開心。🎉

那這個呢?

filesize = "5000" # Size in bytes

在這裡,我們用字串來表示檔案大小。

雖然這樣或許可行,卻是個令人不安的想法。即使是簡單的計算,也可能導致非預期的結果:

file1 = "5000"
file2 = "3000"
total = file1 + file2
print(total) # prints '50003000'

我們該如何修正這個問題?

我們可以放心地假設檔案大小永遠是個數字。更精確地說,它必須是一個正的自然數。不可能有負的檔案大小,而我們最小的記憶體區塊是一個位元組(除了最罕見的系統之外)。而且因為我們面對的是離散的機器,我們知道它只能是電腦能夠處理的檔案大小。如果我們能用一種精確的方式來表達這一切就好了……?

這就是型別系統登場的時候了。
在 Rust 中,你可以定義一個帶有 size 欄位的 File 型別。

struct File {
  name: String,
  size: usize,
}

usize 給了你保證,永遠大到足以容納任何指向記憶體的指標(在 64 位元電腦上 usize = u64)。現在 size 的型別不再有任何模糊之處。你甚至無法建立一個無效的檔案物件:

// Error: `size` can't be a string.
let weird_file = File { name: 123, size: "hello" };

型別系統會防止無效的狀態。它根本不會讓你打破自己訂下的規則。它會讓你為自己的設計選擇負責。敢說一句:它成了你大腦的延伸。過了一段時間,你會開始依賴型別檢查器。「如果能編譯,就能執行」是一句強而有力的箴言。

型別能提升可讀性並提供脈絡

來看看下面這段 Python 程式碼片段:

def filter_files(files):
  matches = []
  for file in files:
    if file.status == 0:
      matches.append(file)
  return matches

0 代表什麼?我們無從得知。我們缺乏脈絡!

一旦我們像這樣定義一個 enum(列舉)型別,故事就變得清楚一些了:

from enum import Enum

class FileStatus(Enum):
  OPEN = 0
  CLOSED = 1

上面範例就會變成

def filter_files(files):
  matches = []
  for file in files:
    if file.status == FileStatus.OPEN:
      matches.append(file)
  return matches

在較大的程式碼基底中,FileStatus.OPEN0 容易搜尋得多。

注意:原生的 enum 型別在 Python 的歷史中是很晚才引進的。它是一個很好的例子,說明了強化型別系統如何有助於提升可讀性。

當你組合不同的型別時,奇妙的事就會發生。

當你明智地選擇型別時,所有拼圖會突然各就各位。編譯器會無端開始檢查你的設計決策,以及你所有的型別是否能良好地協同運作。它會指出你心智模型中的缺陷。這會在重構時給你極大的信心。

舉例來說,讓我們來想想排序這件事。當我想到排序時,我首先想到的是數字清單:

sorted([1,5,4,3,2]) # [1,2,3,4,5]

這是理想的情況。那這個呢?

sorted(1)

哎呀。這行不通,因為 1 是單一數字,而不是集合!如果我們在把資料傳給 sorted 之前忘了檢查型別,程式執行時就會發生錯誤。

sorted([1, "fish"])

在 Python 2 中,這會得到 [1, 'fish']因為字串會以長度來比較

編輯:Reddit 使用者 jcdyer3 指出原因在於當無法比較的型別相互比較時,它們會依型別來排序,所以所有的整數都會排在所有字串之前。這是 CPython 實作細節。)

根據 Python 2,1 < fish
根據 Python 2,1 < fish
來源:插圖由 Freepik 提供

自 Python 3 起,這會拋出例外。

TypeError: '<' not supported between instances of 'str' and 'int'

好多了!少了一個錯誤來源。不過問題在於,這是在執行時期才發生的。這是因為 Python 的動態型別。我們本可以用靜態型別語言來避免這種情況。

fn sorted<T>(collection: &mut [T]) where T: PartialOrd {
  // TODO: Sort the collection here.
}

看起來很嚇人,但其實不然。

我們定義了一個名為 sorted 的函式,它接受一個名為 collection 的輸入參數。

collection 的型別由四個部分組成:

  1. & 表示我們是「借用」這個集合,我們並不擁有它。在函式回傳後,它仍然會存在,不會被清除。
  2. mut 表示集合是可變的。我們被允許修改它。
  3. [T] 表示我們預期輸入是一個清單/切片/向量。其他任何東西都會在編譯時期(在程式執行之前)被拒絕。
  4. PartialOrd 是神奇的關鍵。它是一個 trait(特徵),有點像介面。它表示集合中的所有元素 T 都必須是可部分排序的

所有這些資訊都能幫助編譯器防止我們搬石頭砸自己的腳。而且我們不用去別處查看,就能理解函式的輸入與輸出。

重點整理

  • 型別迫使開發者做好功課,並思考程式碼的保證與限制。
  • 不要把型別想成限制,而是把它們想成一張安全網,能保護你免受自身有缺陷的心智模型的影響。
  • 永遠選擇最能精確表達你意圖的型別。
  • 如果標準函式庫中沒有完美的型別,就用更簡單的型別自己建立一個。

遵循這些規則後,我發現自己被神奇地引導向最優雅地呈現想法的方式。我的程式碼也變得更加符合慣例。

原文由 Matthias Endler 發布

本文章由 muse-spark-1.2-contributor 進行翻譯