iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

30 天的 Functional Programming 之旅系列 第 13

[Day 13] 函數的語言:型別簽章(Type Signature)簡介

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250927/20168201kDPiAWFmL7.png

前言

在上一篇文章中,我們介紹了 const toSlug = pipe(trim, toLowerCase, replaceSpaces, removeExclamation); 這種 Point-Free 風格。它雖然優雅,但也像個黑盒子:如果不看 pipetrimtoLowerCase 的原始實作,我們要怎麼相信資料能順利「流」過這條函數鏈?

這正是大型 JavaScript 專案常見的困難。因為 JavaScript 是動態型別語言,函數的「契約」——輸入與輸出型別——往往是隱性的,只存在於命名、程式碼細節或開發者的腦海裡,導致協作與維護容易出錯。

為了解決這個問題,我們需要一種簡潔的通用語言。在函數式程式設計中,這種「世界語」就是型別簽章 (Type Signature),它源自 Hindley–Milner (HM) 型別系統,是 Haskell、ML、Scala、Elm 等函數式語言社群的共同語彙,今天我們就要來看看它是什麼~

為何我們需要一種「函數的語言」?

作為一種動態型別語言,JavaScript 允許我們在不宣告型別的情況下定義變數與函數。這種彈性讓我們可以快速開發,但隨著應用程式複雜度增加,也帶來了挑戰。

假設我們需要一個工具函數來處理準備要送出的 API 資料,程式如下:

// 一個用來準備 API payload 的工具函數
// 我們必須閱讀程式碼才能知道它到底做了什麼。
// - `data` 的形狀是什麼?一個物件?一個陣列?
// - `config` 裡面有哪些 key?它們都是可選的嗎?
// - 回傳值的樣貌是什麼?有可能是 null 嗎?
function preparePayload(data, config) {
  const payload = {...data };
  if (config.includeTimestamp) {
    payload.timestamp = Date.now();
  }
  if (config.formatter) {
    return config.formatter(payload);
  }
  return payload;
}

要正確且安全地使用 preparePayload 這個函數,開發者只能閱讀它的程式來理解它的型別。函數的簽章實際上散落在程式碼的邏輯判斷中。這模式會讓重構和維護變得困難,有可能不小心就破壞了原有邏輯。

函數簽章(Type Signature):指的是函數所接受的參數型別與回傳值的型別

為什麼不用 TypeScript 就好了?

提到 JavaScript 是動態型別語言,應該有些人會想到:
「既然 JavaScript 是動態型別,我們改用 TypeScript 就好了,為什麼還要學 HM 型別簽章?」

關鍵在於:兩者雖然都能描述函數,但解決的問題層次不同。

TypeScript 的角色:工程級的保護網

  • 目標是「正確性 (Correctness)」,由編譯器強制檢查。
  • 有子型別、交集/聯集型別、條件型別,符合 JavaScript 生態。
  • 語法冗長,描述高階函數時常顯得複雜。
  • TypeScript 的推論結果不一定是最通用的型別。例如:const arr = [1, "a"]; TypeScript 可能會推論它是 (string | number)[]。這沒問題,但其實還有更「廣」的型別描述(例如 any[]),或更「窄」的描述(例如 [number, string])。在 Hindley–Milner 型別簽章的系統裡,這種情況會有一個唯一的、最通用的型別來描述;而在 TypeScript 裡,編譯器只會依照語境挑一個「合理」的型別,而不是保證全域最通用。

另外,TypeScript 並非設計為「完全健全」的型別系統,它的官方設計目標的「Non-goals」就有說:「Apply a sound or "provably correct" type system. Instead, strike a balance between correctness and productivity.」,代表他們「不追求健全」的型別系統,而是追求正確性與生產力的平衡。

Hindley–Milner 型別簽章的角色:數學化的通用語言

  • 目標是「清晰度 (Clarity)」,不是強制執行的工具,而是一種設計與思考的輔助。
  • 只關注函數的輸入與輸出,以及這些型別間的抽象關係(參數化多型、柯里化)。
  • 語法極度簡潔,例如:map :: (a -> b) -> [a] -> [b],但在 TypeScript 中則需寫成:const map = <A, B>(fn: (x: A) => B, arr: A[]): B[]
  • Hindley–Milner 的簡潔與抽象,讓它成為函數式社群的「世界語」:在 Haskell、ML、Scala、Elm、Ramda.js 的文件中都能看到它,不受限於特定程式語言。

換句話說:

  • TypeScript 型別:像是一份詳細的工程規格書,精確而繁瑣,幫助編譯器守護你的程式。
  • HM 型別簽章:像是一行數學公式,簡潔地揭示函數的核心行為,跨語言、跨社群都能看懂。

因此學習 HM 型別簽章的價值,不在於它取代 TypeScript,而在於它幫助我們以更純粹的語言來思考函數的本質。

有與沒有型別簽章的差別

為了感受型別簽章帶來的改變,我們來看一個在函數式程式設計中很常見的高階函數範例。

使用型別簽章之前

假設我們需要一個函數,可以根據指定的屬性名稱,將一個物件陣列分組。在沒有型別簽章的情況下,我們可能會寫出這樣的程式碼:

// 將物件陣列按照指定的屬性進行分組
const groupBy = (key, list) => {
  return list.reduce((acc, obj) => {
    const group = obj[key];
    acc[group] = acc[group] || [];
    acc[group].push(obj);
    return acc;
  }, {});
};

const users = [
  { id: 1, name: 'Alice', role: 'admin' },
  { id: 2, name: 'Bob', role: 'user' },
  { id: 3, name: 'Charlie', role: 'user' },
  { id: 4, name: 'David', role: 'guest' },
  { id: 5, name: 'Eve', role: 'admin' }
];

console.log(groupBy('role', users)); // groupBy 的回傳值是什麼結構?我們必須執行它或仔細閱讀程式碼才能確定。
/*
{
  admin: [
    { id: 1, name: 'Alice', role: 'admin' },
    { id: 5, name: 'Eve', role: 'admin' }
  ],
  user: [
    { id: 2, name: 'Bob', role: 'user' },
    { id: 3, name: 'Charlie', role: 'user' }
  ],
  guest: [
    { id: 4, name: 'David', role: 'guest' }
  ]
}
*/

要理解這個 groupBy 函數,我們必須在腦中逐步執行 reduce 的邏輯:它接收一個字串 key 和一個物件陣列 list,然後回傳一個新的物件。這個新物件的鍵 (key) 是從原物件中取出的屬性值,而值 (value) 則是包含原始物件的陣列。整個過程需要花費心力去解讀與推斷。

使用型別簽章之後

現在,讓我們為這個函數加上一行 Hindley–Milner 風格的型別簽章註解:

// groupBy :: String -> [a] -> { String: [a] }
const groupBy = (key, list) => {
  //... 相同的實作...
};

透過這行註解,groupBy 就變清晰了。這個簽章告訴我們:

  • 它接收的第一個參數是 String(用來分組的鍵)。
  • 第二個參數是 [a](一個由任何型別 a 的元素組成的陣列)。
  • 它最終回傳 { String: [a] }(一個物件,其鍵為 String,其值為與輸入陣列相同型別 a 的元素組成的陣列)。

模糊地帶消失了,我們不再需要閱讀實作細節,就能理解這個函數的行為。

https://ithelp.ithome.com.tw/upload/images/20250927/20168201LJu32qV9jL.png
圖 1 使用型別參數前後的示意圖(資料來源: 自行繪製)

參考上圖,我們可以看到前後對比,左邊是使用型別參數前,groupBy 的輸入和輸出都未知,右邊的 groupBy 同樣是黑盒子,但我們可以知道輸入是 String[a],輸出是 { String: [a] }

所以,型別簽章到底是什麼?

接下來讓我們深入探討型別簽章,並說明其語法。

解構語法:閱讀函數的說明書

Hindley-Milner 風格的簽章就像一套精簡的語法,現在來逐一拆解它的組成。

  • :::是類型標註符號(type annotation symbol),類似「類型聲明運算子」,可將名稱與類型建立關聯。簡單來說,可以讀作「...的型別是」(has type of)。它將函數名稱與其型別定義分開。例如 head :: 就表示「head 函數的型別是...」。
    • 補充一下,:: 有點類似 TypeScript 的 :,例如在 TypeScript 中我們會寫 let age: number = 25; 來聲明 agenumber 型別,在 Hindley-Milner 型別簽章中就會用 head :: 來表示 head 函數的型別
  • ->:這是最重要的分隔符。它分隔了參數與回傳值。一個簡單的規則是 「最後一個箭頭右邊的,永遠是最終的回傳值型別」 。例如 a -> b -> c 代表這個函數最終會回傳一個型別為 c 的值。

先從最簡單的具體型別開始看:

// parseInt :: String -> Number
// toUpperCase :: String -> String

第一行表示 parseInt 是一個接收 String 並回傳 Number 的函數。第二行表示 toUpperCase 接收 String 並回傳 String。非常直觀 👍

以下再用一個表格來分別說明 capitalize :: String -> String 各部分對應的意義。

部分 代表的意義 對應概念
capitalize 函式名稱,表示要執行的操作,例如將字串首字母轉大寫 函式(Function)
:: 類型標註(type annotation),表示 capitalize 的類型 類型標註符號(Type Annotation Symbol)
String 函式的輸入類型,表示 capitalize 接受一個字串 參數類型(Input Type)
-> 函式映射關係,表示函式從一個類型轉換到另一個類型 函式類型箭頭(Function Arrow)
String 函式的輸出類型,表示 capitalize 回傳一個字串 回傳類型(Output Type)

根據上述,capitalize :: String -> String 就是表示 capitalize 這函數接受一個 String 參數,並且會回傳 String

補充:Hindley–Milner (HM) 型別系統的起源

HM 型別系統的想法最早出現在 1969 年,當時數學家 J. Roger Hindley 在研究「組合子邏輯」時,提出了「最一般型別 (principal type)」的概念。簡單來說,就是如果一段程式能被加上型別,那一定存在一個「最通用」的型別方案,其他的型別都可以從它推導出來。

幾年後的 1978 年,電腦科學家 Robin Milner 在設計 ML 語言(早期一種的函數式語言)時,重新發明並應用了這想法。他的目標很是讓程式同時「安全」又「好寫」。程式設計師不用到處寫型別註解,編譯器就能自動幫你推算出正確的型別,還能在編譯時期就避免型別錯誤。

到了 1982 年,Luis Damas 與 Milner 一起把這整套系統嚴謹地整理成理論基礎,這才有了後來完整的 Hindley–Milner 型別系統(有時也叫 Damas–Milner 系統)。它成為 Haskell、OCaml、F# 等函數式語言背後的重要基礎。

HM 系統誕生的原因就是為了解決一個痛點:怎麼讓我們同時擁有靜態型別的安全性,又不用寫一大堆型別註解? 答案就是靠強大的型別推論 (type inference),讓編譯器自動幫你找出「最一般型別」。

參數化多型:ab 的魔力

在程式設計中,「多型」(Polymorphism) 的核心思想是讓一個介面或符號能適用於不同型別的實體,簡單來說,就是讓一份程式碼能處理多種資料型別。在函數式程式設計中,我們最關心的是「參數化多型 (Parametric Polymorphism)」。參數化多型的核心思想在於,函數或資料結構的撰寫方式是通用的,它能以完全一致的邏輯來處理各種型別,而不需依賴它們的具體內容。一個參數化多型的函數,其行為對於它所操作的所有型別都是統一的 。  
 
在 HM 簽章中,我們使用小寫字母(通常是 a, b, c...)作為型別變數 (type variables)。它們就像是型別的佔位符,代表「任何型別」。

補充:參數多型 (Parametric polymorphism)
參數多型在程式設計語言與類型論中是指聲明與定義函數、複合類型、變數時不指定其具體的類型,而把這部分類型作為參數使用,使得該定義對各種具體類型都適用。

範例 1 (單一泛型): a -> a

// head :: [a] -> a
// 接收一個由「任何型別 a」組成的陣列,並回傳一個「相同型別 a」的元素。
const head = (arr) => arr[0];

head([1, 2, 3]);         // a 是 Number,回傳 Number (1)
head(['x', 'y', 'z']);   // a 是 String,回傳 String ('x')

這裡的關鍵是,a 在簽章中出現了兩次。這是一個約束:如果輸入的陣列是 [Number],那回傳值必須是 Number,不可能是 Stringnull

範例 2 (型別轉換): a -> b

當函數涉及到型別轉換時,我們會使用不同的型別變數。map 函數是最好的例子:

// map :: (a -> b) -> [a] -> [b]
// 1. 接收一個函數,這個函數能將型別 a 轉換為型別 b。
// 2. 接收一個由型別 a 組成的陣列。
// 3. 回傳一個由型別 b 組成的陣列。
const map = (fn, arr) => arr.map(fn);

const numbers = [1, 2, 3];  // a 是 Number
const double = (n) => n * 2;       // double :: Number -> Number
const toString = (n) => String(n); // toString :: Number -> String

map(double, numbers);   // 在此例中,a 和 b 都是 Number。回傳 [2, 4, 6]
map(toString, numbers); // 在此例中,a 是 Number,b 是 String。回傳 ['1', '2', '3']

map 的簽章詮釋了它的多功能性。ab 可以是相同的型別,也可以是完全不同的型別。這個簽章本身就幾乎完整的說明了 map 的功能。

Currying(柯里化)的型別簽章

Currying(柯里化)是 FP 重要的元素之一,HM 簽章的語法設計和 Currying 天生契合,因為箭頭 -> 是向右結合的,這代表 a -> b -> c 在解析時會被視為 a -> (b -> c),也就是傳入 a 參數會獲得一個 b -> c 的函數,這剛好是 Currying 函數的本質。  

// add :: Number -> Number -> Number
const add = a => b => a + b;

const add5 = add(5); // add(5) 的回傳值是什麼型別?
                     // 根據簽章 a -> (b -> c),5 對應第一個參數 a(Number),現在 add(5) 回傳的是 Number -> Number

add 的簽章告訴我們,它是一個接收 Number,並回傳一個「接收 Number 並回傳 Number 的新函數」的函數。剛好對應柯里化的實作。

再看一個更複雜的例子:

// replace :: RegExp -> String -> String -> String
const replace = pattern => replacement => str => str.replace(pattern, replacement);

這個簽章可以這樣解讀:replace 是一個接收 RegExp,回傳一個新函數(String -> String -> String);這個新函數接收 String(替換內容),再回傳另一個新函數(String -> String);這最後一個函數接收 String(原始字串),最終回傳 String。型別簽章完整地描述了整個柯里化的鏈條。

其他範例

第一次接觸型別簽章時,很容易迷失,所以這裡又再列了一些範例來理解型別簽章~希望能多看多熟悉,因為後續介紹 FP 工具,都會用型別簽章來說明函數的輸入輸出。

// filter :: (a -> Bool) -> [a] -> [a]
// 接受 (a -> Bool) 函式與 a 陣列 [a],回傳符合條件的 a 陣列 [a]
const filter = curry((f, xs) => xs.filter(f));

// reduce :: ((b, a) -> b) -> b -> [a] -> b
// 第一個參數 ((b, a) -> b) 是一個函式,接受 b (累積值)和 a(當前值),回傳 b (新累積值)
// 第二個參數 b 是初始值
// 第三個參數 [a] 是 a 的陣列,對應要處理的陣列資料
// 最終結果是 b,reduce 在 a 陣列上進行累積運算,最後得到累進值 b
const reduce = curry((f, x, xs) => xs.reduce(f, x));

// match :: Regex -> String -> [String]
// 接受 Regex 和 String,回傳 String 陣列 [String]
const match = curry((reg, s) => s.match(reg));

// join :: String -> [String] -> String
// 接受一個 String 和一個 String 陣列 [String],回傳 String
const join = curry((what, xs) => xs.join(what));

與 TypeScript 的對照

雖然 TypeScript 和 HM 簽章代表的意義不同,但還是有些相似處,以下比較 HM 簽章與 TypeScript 的泛型語法,展示它們如何用不同的語法來表達相同的核心思想。

概念 Hindley-Milner 標記法 TypeScript 標記法
具體型別函式 (Simple Function) isEven :: Number -> Boolean const isEven = (n: number): boolean => ...;
單一泛型 (Generic, Same Type) head :: [a] -> a const head = <T>(arr: T[]): T => ...;
泛型轉換 (Generic, Transform) map :: (a -> b) -> [a] -> [b] const map = <T, U>(fn: (x: T) => U, arr: T[]): U[] => ...;
柯里化 (Currying) add :: Number -> Number -> Number const add = (a: number) => (b: number): number => ...;
高階函數 (Higher-Order Function) applyTwice :: (a -> a) -> a -> a const applyTwice = <T>(fn: (x: T) => T, value: T): T => fn(fn(value));

上述表格中,我們可以觀察到,雖然兩者都能表達參數化多型,但對於函數式程式設計中常見的高階、柯里化函數,HM 的語法通常更簡潔、更容易閱讀。

Theorems for Free

在 Philip Wadler 的論文 《Theorems for Free!》 中,他指出:一個足夠多型的型別簽章,已經對函數的可能實作施加了強大的邏輯限制。因此,我們僅透過閱讀型別簽章,就能直接推導出一些「自由定理 (free theorems)」,這些定理描述了函數必然遵守的行為規律,而無需檢視或撰寫實際程式碼。

範例 1: identity 的必然性

// identity :: a -> a

我們可以做一些邏輯推導:

  1. 這個函數簽章承諾,它對任何可能的型別 a 都有效。
  2. 它接收一個型別為 a 的參數。
  3. 函數本身對 a 一無所知。它不知道 a 是否為 String 而擁有 .length 屬性,也不知道 a 是否為 Number 而可以進行加法運算。它沒有任何可以對 a 進行的特定操作。
  4. 它也無法無中生有地創造一個新的、型別為 a 的值。
  5. 因此,唯一能夠滿足這個契約,且對所有可能的 a 都成立的合法實作,就是原封不動地回傳它所接收到的那個參數

結論:a -> a 這個型別本身就證明了任何實現它的函數必然是恆等函數 (identity function)。這就是我們免費得到的第一個定理,透過型別簽章理解了 identity 函數的實作,且不需看他的程式碼內容。

範例 2: head 的來源保證

現在來看另一個例子:

// head :: [a] -> a

一樣做一些邏輯推導:

  1. 這個函數承諾會回傳一個型別為 a 的值。
  2. 它能從哪裡得到這個值呢?
    • identity 一樣,函數對 a 一無所知,所以它無法創造一個 a
    • 唯一能獲得 a 型別值的地方,就是從輸入的 [a] 陣列中取得。

結論:這個簽章證明了 head 函數的回傳值必定來自於輸入陣列中的某個元素。它不可能回傳一個 hard code 的 42null(除非 a 恰好就是 NumberNull 型別),因為那樣會違反它對「所有」型別 a 都有效的承諾。

型別簽章與程式碼的可重構性

free theorems 可為程式碼的重構提供數學面向的的保證。

讓我們思考一個型別為 f :: [a] -> [a] 的函數,例如 reversesort。根據我們之前的推導,這個函數因為對 a 一無所知,所以它能做的操作僅限於:重新排序元素、複製元素、或丟棄元素。它不可能改變元素自身的內容(例如把一個數字加一,或把一個字串轉為大寫),因為如果改變了,回傳值就是 [b] 而不是相同的 [a]。它對元素的值是「無知」的。

現在,我們將這個「無知」的函數 f 與我們熟悉的 map 函數組合起來。map 的作用恰恰相反,它的目的就是用一個轉換函數 g 來改變陣列中的每一個元素。

我們有兩種組合方式:(💡 提醒:compose 是由右到左執行)

  1. compose(map(g), f):先用 f 重排陣列,然後用 map(g) 轉換每個元素。
  2. compose(f, map(g)):先用 map(g) 轉換每個元素,然後用 f 重排轉換後的陣列。

因為 f 對元素的值是無知的,它根本不在乎自己操作的是原始的元素,還是被 g 轉換過的元素。它執行的重排、複製、丟棄邏輯都會是完全一樣的。這就導出了一個的 free theorems:

map(g). f = f. map(g)
(補充,. (點)這個符號代表函數組合 (function composition),f. g 的意思就是「先執行 g 函數,然後把 g 的結果作為輸入傳給 f 函數」。換句話說,(f. g)(x) 就等同於 f(g(x))。)

更白話理解這公式,意思就是 「先換顏色再洗牌」和「先洗牌再換顏色」結果是相同的

https://ithelp.ithome.com.tw/upload/images/20250927/20168201DU9tlQQSl8.png
圖 2 「先換顏色再洗牌」和「先洗牌再換顏色」結果是相同的(資料來源: 自行繪製)

map(g). f = f. map(g) 不僅是一個學術上的等式,還是一條可以安全應用的程式重構規則。它證明了這兩種組合方式在結果上是完全等價的。以下用程式說明:

// reverse :: [a] -> [a]
const reverse = arr => [...arr].reverse();

// toUpperCase :: String -> String
const toUpperCase = str => str.toUpperCase();

// map :: (a -> b) -> [a] -> [b]
const map = (fn, arr) => arr.map(fn);

const list = ['a', 'b', 'c'];

// 路徑 1: 先 reverse,再 map
// map(toUpperCase, reverse(list))
// -> map(toUpperCase, ['c', 'b', 'a'])
// -> ['C', 'B', 'A']

// 路徑 2: 先 map,再 reverse
// reverse(map(toUpperCase, list))
// -> reverse(['A', 'B', 'C'])
// -> ['C', 'B', 'A']


// 結果完全相同。型別簽章已預言了這一切!
console.log(map(toUpperCase, reverse(list))); // ['C', 'B', 'A']
console.log(reverse(map(toUpperCase, list))); // ['C', 'B', 'A']

另外也用下圖說明型別簽章對函數的約束力,即使我們不知道函數內部實作,也能確定輸入輸出的樣貌,不會有非預期的值(例如 🍌)出現。
https://ithelp.ithome.com.tw/upload/images/20250927/20168201cL3YpqToVx.png
圖 3 型別簽章約束函數的行為,衍伸出 free theorems(資料來源: 自行繪製)

補充:用型別簽章搜尋函數 (Hoogle)

free theorems 展示的「型別約束行為」的原則,促使了一種應用:用型別簽章來搜尋函數。

Haskell 有一個名為 Hoogle 的 API 搜尋引擎,它讓開發者能透過型別簽章來尋找需要的函數。

假設某天你忘記了「檢查一個元素是否存在於陣列中」的函數叫什麼名字。但你知道它的「形狀」:它應該接收一個元素(型別為 a),一個該元素的陣列(型別為 [a]),然後回傳一個布林值(Bool)。所以,它的型別簽章應該是 a -> [a] -> Bool
我們可以用 a -> [a] -> Bool 在 Hoogle 中直接搜尋這個型別簽章,它會回傳所有符合或近似符合這個簽章的函數列表,如下圖:

https://ithelp.ithome.com.tw/upload/images/20250927/20168201KJIqghjUkc.png
圖 4 Hoogle 截圖示意
在這種情況下,elem 函數(等同於 JavaScript 的 includes)就是搜尋結果之一 。  
能實現這功能的原因就是參數化多型。a -> [a] -> Bool 的簽章限制了函數的可能行為,使型別本身成為一個有用的搜尋關鍵字。

補充:TypeScript 與 fp-ts

在前端開發中,我們不能直接用 Haskell,但可以用 fp-ts 這樣的函式庫來理解 FP。fp-ts 參考 Haskell/Scala 的抽象(Functor/Applicative/Monad…),並用型別編碼實作 Higher-Kinded Types(HKTs)的效果,以在 TS 中擁抱「以型別驅動的抽象」。因此我們可以在 TS 中使用熟悉的 Haskell 風格型別類(Option/Either/Task…),以簽章先行治理副作用與錯誤處理。

以上 Functor/Applicative/Monad 或是 Higher-Kinded Types 這些名詞不懂沒關係,簡單來說 fp-ts 用 TypeScript 實作了許多工具來模擬 Haskell 這種專門 FP 的語言,因此參考 fp-ts 可以讓我們更接近、更了解 FP 的世界。

小結

用三個問題總結今天的文章。

為什麼要有型別簽章?

為了對抗動態型別 JavaScript 的模糊性。它們是明確的契約,能改善團隊溝通、降低認知負擔,並迫使我們在函數式程式設計這個高度抽象的世界中,帶著清晰的意圖去設計函數。

沒有跟有的區別是什麼?

這是一個從「實作優先」(閱讀程式碼來猜測意圖)到「契約優先」(閱讀簽章來理解行為)的轉變。

所以型別簽章是什麼?

它是一種源自 FP 世界的、簡潔而強大的標記法,用以描述函數的介面。它不僅僅是文件,其對參數化多型 (a -> b) 的支持,更提供了 free theorems——關於函數行為的邏輯保證,讓我們能進行更安全的推理與重構。它是一種輔助思考的工具。

Reference


上一篇
[Day 12] Point-Free 是什麼?
系列文
30 天的 Functional Programming 之旅13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言