在 Typescript 中 實(shí)現(xiàn) ADT(Algebra Data Types)

Algebra Data Types,代數(shù)數(shù)據(jù)類型,是一個(gè)函數(shù)式編程中非常有趣的概念,從 java 角度來看,ADT 可以認(rèn)為是一種模式,它看著就像帶上數(shù)據(jù)的枚舉類型,似乎平平無奇,但使用代數(shù)數(shù)據(jù)類型能讓我們更好地進(jìn)行類型建模,表達(dá)更多的東西并避免運(yùn)行時(shí)異常(典型如空指針)。
問題在哪里?
考慮這樣的一個(gè)(簡化的)場景,我們要去實(shí)現(xiàn)一個(gè) TODO APP,其中每條 TODO 稱為 Task,Task 有如下性質(zhì):
Task 有四種狀態(tài):已完成,未完成
Task 有兩種種類:某日的 Task,某日期區(qū)間的 Task(即該 Task 在某日期區(qū)間內(nèi)都持續(xù)進(jìn)行,比如進(jìn)行三天的旅行,摸魚一個(gè)星期……)
于是,設(shè)計(jì)出數(shù)據(jù)庫表后,我們編寫了這樣的實(shí)體(Java 語言描述):
Java 開發(fā)時(shí)一般來說就是這么干的(枚舉可能換成數(shù)據(jù)字典項(xiàng)啥的以節(jié)約空間,方便序列化),但這里有一個(gè)嚴(yán)重問題:
一些字段和其它字段是相互關(guān)聯(lián)的,并且這種關(guān)聯(lián)性并未在類型定義上體現(xiàn)出來,比如對(duì)于 startDate 和 endDate,其顯然在 type 為 DURATION 的時(shí)候才有意義即非 null;對(duì)于 taskDate,其只在 type 為 DAY 時(shí)有意義;對(duì)于 doneDate,其只在 status 為 DONE 時(shí)有意義……這種關(guān)聯(lián)性必須通過文檔,注釋等手段去表達(dá)。
而且,要獲取這些相關(guān)聯(lián)的數(shù)據(jù)時(shí),必須要先對(duì) type 和 status 進(jìn)行判斷才能保證正確,但若程序員因不熟悉業(yè)務(wù)等情況忘記進(jìn)行判斷會(huì)如何?bug 可能就要出現(xiàn)了,而編譯器并管不了這個(gè)。
Java 中的 ADT
解決方案呢?我們可以讓 Task 成為一個(gè)富血對(duì)象,嚴(yán)格限制訪問域,在 getter,setter 中去實(shí)現(xiàn)約束,比如下面實(shí)現(xiàn)一個(gè) startDate 的 getter:
這能強(qiáng)迫使用者去關(guān)心相關(guān)的約束,但仍舊顯得很繁瑣,遺憾的是在 Java 添加 sealed 關(guān)鍵字和模式匹配之前,將 ADT 應(yīng)用在業(yè)務(wù)代碼上確實(shí)沒有多少合適的解決方案。
一個(gè)可能的解決方案是什么呢?我們可以把和 Task 的狀態(tài)相關(guān)的字段和這個(gè)狀態(tài)綁在一起,比如對(duì)于 TaskStatus 這個(gè)狀態(tài),我們可以為每個(gè)情況定義相應(yīng) Class:
“帶上數(shù)據(jù)的枚舉類型”!在實(shí)際操作時(shí),對(duì) status 就必須使用 instanceof 去判斷它的類型了,判斷后進(jìn)行類型強(qiáng)轉(zhuǎn)即可獲取其值。
但這里仍舊有一個(gè)問題:強(qiáng)轉(zhuǎn)是向下轉(zhuǎn)型,仍舊需要使用者去確定強(qiáng)轉(zhuǎn)的類型,且 IDE 不一定補(bǔ)全正確的類型,因此有一定心智負(fù)擔(dān),且仍舊可能出錯(cuò)。
一個(gè)簡單的優(yōu)化方案是在 TaskStatus 中添加相應(yīng)方法去獲取數(shù)據(jù):
在 Kotlin,Scala,Typescript 語言中去實(shí)現(xiàn)的 ADT 實(shí)際上在很大程度上和這種方法(模式)是一致的,但是搭配上這些語言的模式匹配功能(以及 sealed, case 等關(guān)鍵字),用起來就會(huì)順滑無比,從而真正在工程實(shí)踐中有應(yīng)用價(jià)值。
Typescript 中的 ADT
在 Typescript 中去實(shí)現(xiàn) ADT 輕而易舉,因?yàn)樗旧砭桶^的或(積 Product)類型 (|
運(yùn)算符),比如上面的 TaskStatus 可以直接這么表達(dá):
Typescript 足夠聰明,用戶只需要對(duì) _tag 進(jìn)行判斷,它就能夠知曉該數(shù)據(jù)的類型究竟是 TaskDone 還是 TaskNotDone,因此我們可以用 switch 去做簡單的模式匹配(并且輸入 case 的時(shí)候也能得到補(bǔ)全!)。
React 的 Reducer 的 Action 類型也可以看作 ADT,這時(shí)它的 type 屬性就代表它的類型:
但在這里有一個(gè)問題——這樣操作的話沒法往 ADT 上面添加方法了,這對(duì)面向?qū)ο笳Z言還是非常難受的,但仍舊可以解決,如下面的代碼實(shí)現(xiàn) Typescript 版的 Maybe:
這就舒服了。
tips: Haskell 的 ADT 類型定義類似data Maybe a = Nothing | Just a
,其中 Maybe 稱為類型構(gòu)造器,Nothing 和 Just 對(duì)應(yīng)值構(gòu)造器,Maybe 對(duì)應(yīng)這里的 Optional 類型,Nothing 和 Just 對(duì)應(yīng)兩個(gè)同名函數(shù)
參考資料
《Algebraic Data Types in TypeScript》,https://www.susanpotter.net/software/algebraic-data-types-in-typescript/