圖靈獎得主Dijkstra在2001年寫信給德州大學預算委員會,希望大學的程式設計入門課程,不要使用命令式的Java取代函數式的Haskell。事實上,2001年的前後是Java 2的年代,當時Java 1.3推出沒多久,無怪乎Dijkstra會覺得Java看來就像個商業宣傳,因此,完全可以想像Dijkstra為何對此感到不安。

雖然我本身也是函數式典範的愛好者,然而,基於十幾年來的使用經驗,不得不承認函數式有許多更高程度的抽象,因為它們更接近數學,而不是身邊常見的具體現象或直覺;其實我本身不擅長從抽象理解抽象,很多時候是有了夠多的具體經驗,回頭看看函數式中的某些概念,才終於得到「原來是這麼一回事」的體會。

Java越來越Haskell

從2001年至今二十多個年頭的過程,許多命令式程式語言、程式庫或框架,逐步調整、融入了許多函數式的元素,要說短視近利也好,要說為實務而用也好,這個漫長的過程,就今日來看是必要的。

因為在這各調整、融合的過程中,有成功與失敗的案例,也出現了新的應用場合,而等到開發者們累積夠多的具體經驗之後,逐漸知道怎麼在命令式的世界中適切地闡述、決定函數式元素,該以什麼樣貌出現。

有趣的事情是,Scala差不多也是在2001年開發、2003年左右發表第一個版本,就我個人而言,第一次從Scala接觸到函數式概念時,雖然感到震撼,然而,也不能完全深入體會其中各種函數式元素,像是case、sealed類別等,就我個人而言,當年的Scala很前衛、很有遠見,可惜推出的時間點稍早,走得稍快了些。

回頭看看保守的Java,2014年才終於有了lambda等基本的函數式元素,稍微可以跟上Python、JavaScript等具有一級函式的語言腳步,獲得一些高階流程抽象的方式可以使用,在之後幾年的經驗累積後,終於開始納入了函數式中更為基礎的元素「代數資料型態」,具體而言,就是有了record與sealed類別,這也讓我有一種感覺:Java越來越Haskell!

Haskell的sum型態/product型態

Haskell有Bool型態,關於布林值,要不就是True,或要不就是False,因此,Bool型態的定義方式,基本上,就是data Bool = True | False,而這種交替(alternation)構成的型態,類似集合A+B的概念(這邊的+代表互斥聯集),所以稱為sum型態。

此時,你可能會想到Java的列舉(enum)。單獨看Haskell的sum型態確實像是列舉,然而,sum型態還可以結合product型態,Java的列舉卻沒辦法做到這點,因為,能約束子類型態的sealed類別,才是接近sum型態概念的元素。

想瞭解product型態,可從tuple開始。你可以用pt=(1.1,2.2)代表座標,也可以用vt=(0.5,0.5)代表向量,move pt vt表示pt移動了vt的量,問題在於:如果我寫了(1,2),這是座標還是向量呢?(10,20)==(10,20)的結果會是True,如果我說==左邊是座標右邊是向量,結果True是對的嗎?

顯然地,當你面對方才的問號時,就表示使用tuple不再足夠了,你需要為這些資料定義型態名稱,以便能從進一步從型態名稱來區別資料。在Haskell,我們可以分別定義data Point = Point Float Float,以及data Vector = Vector Float Float,這麼一來,Point 10 20與Vector 10 20就不會混淆。

也就是說,一個型態可以是多個型態的結合(combination),新型態的值會是用來結合的型態值之集合乘積(product),例如,data Foo = Foo Bool Bool,由於Bool有True與False,Foo型態的值就會有四個可能組合,因此Foo是一種product型態。

sum與product可以結合來定義新型態,例如,data Shape = Rectangle Point Float Float | Circle Point Float,也就是形狀有長方形及圓形,只不過那些欄位代表什麼呢?

Haskell有record語法,我們可以指定資料的各欄位名稱,例如,Shape的Rectangle,可以定義為Rectangle {center :: Point, leng :: Float, width :: Float},這樣是不是清楚多了呢?record語法進一步揭露了欄位名稱,而Java 16的關鍵字record,就是從Haskell的record語法來的!

代數資料型態?

Haskell的sum型態/product型態,還可以遞迴地定義,或者是結合型態參數等語法,讓開發者在定義型態時更為便利,而這會牽涉到更多語法細節,有興趣的話,可以參考〈代數資料型態〉。

不過,許多開發者心中應該都會有疑問,到底上述這種方式定義的型態,為什麼要稱之為代數資料型態呢?

因為sum與product聽來就很代數?不!想想看,我們從小是怎麼開始學數學的呢?先從認識整數1、2、3……開始,對吧!接著學習在這些整數上進行加法,後來是減法,接著是乘、除等操作,這就是代數的入門,以符號(例如1、2、3……)標記數,研究數之間的各種關係與性質,就是代數的範疇(可參考維基百科〈代數〉條目)。

方才定義Bool、Point、Vector時,並未涉及運算的定義,單純只是定義型態罷了,如果Point與Point之間要執行相加、比較等運算,可以在後續其他場合進一步定義。換句話說,型態的定義與運算的定義是分開的,如果某型態需要某運算,後續只要增加新函式就可以了。

接著,我們在數學上的學習,又加上了分數、小數、負數,對吧?也就是數與數之間的結合,產生了更多的數,然後發展出更多的運算,像是平方、開根號……,這就像sum與product可以結合來定義新型態,也從而發展出更多的函式來做運算。

而這就是與物件導向強調的抽象資料型態之間的最大差別,在定義抽象資料型態的同時也要定義運算(方法),另一方面,相較於代數資料型態的結構都是公開的,抽象資料型態需決定必須揭露的狀態,以及必須隱藏的狀態。

關於兩種資料型態的定義方式,端看開發者習慣用何種方式來思考,我在先前專欄〈模式比對與多型〉也談過,它們各有適用情境,而Java加入的record與sealed類別,目的是為開發者多提供選擇。

Haskell的代數資料型態

雖然在Java就算不知道「代數資料型態」這個名詞,從其他角度來認識record與sealed類別,多半也能掌握其應用方式(參考先前專欄〈不只是語法糖的記錄類別〉、〈揭露型態邊界的彌封類別〉),這表示代數資料型態經過這麼多年的磨合,也算是適切找到了平衡,不再被視為學術性的概念。

對此,Dijkstra若知道二十幾年後,Java加入這麼多函數式元素,甚至是代數資料型態的概念,會不會稍微感到寬慰呢?

就我個人的經驗而言,record與sealed類別多少還是加上了一些迷霧,會讓開發者看不清代數資料型態的本質;如果可以,建議你直接從Haskell認識代數資料型態的定義與應用,這對於record與sealed類別的使用,會有極大的啟發,更能掌握應用的場景,在一些具有類似元素的語言中,也會有很大的幫助。

專欄作者


熱門新聞

Advertisement