悅耳的類型系統

閱讀時間約 11 分鐘

物件導向程式語言的類型系統總是不合理的,這些程式語言承襲至舊的語言不好的特性,而人們並沒有意識到它的問題,或者比起健全(soundness)它們更注重熟悉(familiarity)。多數的物件導向程式語言都源自c/c++,而c++有太多糟糕的設計,然而同一時期出來的Haskell卻很少有類似的問題。Haskell是基於數學理論的程式語言,而且它是高階程式語言,因此自然在抽象概念的合理性上有更多的要求。然而這種合理性不只侷限於他背後的數學理論,更可應用於其他程式設計的概念。事實上Haskell包含很多相關但相互獨立的特性,每個特性都可以單獨抽離出來使用,其中上一篇文章討論的代數資料類型就是一個例子。因此我們不應該將這種合理性片面地視作是學術且數學的,它對於我們理解程式語言以及如何寫好程式也有巨大的幫助。前幾篇文章已經提過一些不合理的設計,它們大多都藏在難以發現的地方或是因為習慣而被我們忽視。這篇文章將會討論合理性的重要性。


在這裡「不合理」並不代表違反規則,而是在實際使用上我們會如何看這些特性。例如即使我們知道被宣告成指標的變數可能為空,但我們仍然會不小心觸發空指標的錯誤,就算c++有提供非空指標給你使用,這種錯誤仍層出不窮。這有兩個原因。第一,「指標」的規則設計問題,指標被設計成存取成員變數或方法時會在空指標時出錯,這個規則使得我們必須透過上下文自己判斷是否合法。然而這仰賴於開發者的細心程度,這是軟體工程裡最不能相信的一件事。站在開發者的角度,這種莫名奇妙的規則有很糟的開發體驗。第二,「指標」(pointer)概念的語義模糊,指標代表的應該是指向某個特定類型物件的間接參考,但有空指標這一個特例。因此c++發明了的「非空指標/參考」(reference)以排除這個特例。反過來說,指標這個詞指的其實是參考和空指標的聯集。然而我們對指標這個詞的認識仍著重在指向物件這件事,常常忽略空指標的特例,這在Java這種刻意消除指標概念的程式語言尤其嚴重。rust的解決辦法是將聯集的部分抽取出來,因此c++的指標int*在rust就變成兩部分Option<&int>,如此設計更清楚地描述了指標和可能為空這兩個特性。指標在c++的定義很明確,規則雖然有點複雜但還不到難以理解的地步,但這種指標的概念在實際使用上容易出錯,甚至會造成理解上的差異。它的規則不是不合理,找到bug時原因也都能理解,但就是「不符合預期」,也就是跟我們心中的模型有所誤差。不合理的是程式語言的設計,這種設計對於機器的實際運作方式來說非常合理,但程式語言是給人類寫的,應該根據人類的思考方式設計。好的程式語言使用起來會感到很合理(指標就是指標,使用它底下的方法不會有任何問題),很和諧(若指標可能為空,就用Option,它並不是指標特有的;例如Iterator::find可能找不到東西,所以也會回傳Option),很賞心悅目(編碼時會跟著音樂一起嗨)。因此這篇的文章標題取為「悅耳的類型系統」(A sound type system, 故意翻錯的)。


物件導向相對於函數式編程概念都稍微有點「歪斜」:如果我們把程式語言的特性與概念一一列出,並從函數式編程的角度去分類,再依據物件導向的角度分類,會發現被圈在一起的部分幾乎跟函數式編程一樣,但是就是有一些地方不同,因此說它有一點歪斜。物件導向跟函數式編程其實很相似,抽象,封裝、多型、介面等概念函數式編程也都有,但就是那微小的差異使得物件導向變得難用不合理。有人認為如果用正確的方式使用物件導向,結果會很像函數式編程。但問題是物件導向程式語言的設計使得開發者容易出錯,例如明明最重要的概念就是繼承,但正確的做法卻是少用繼承多用介面;提倡盡量以組合代替擴展,但卻又允許以繼承改寫方法;建議把帶有狀態邏輯最小化,但物件本身就是狀態機。當有多種方法可以實現功能時,開發者往往會選擇最明顯的方法實作,而物件導向的問題就是最明顯的常常是不好的方法。為了避免開發者持續產出糟糕的程式碼,跑出了各種物件導向的開發原則和設計模式,這些主要都是為了「修復」物件導向的缺點,很多概念放到函數式編程就會變得稀鬆平常、廢話連篇。因此並不只是物件導向相對歪斜,而是它本來就是歪的。


這種歪斜不一定是壞的,在某些情況物件導向可以給出不同的觀點。舉一個實際的例子:我公司的同事最近在c#實作了Result class,但實作方式很有趣。他把Result<Value,Error>實作成Result<Error>的子類,為了方便起見我把前者重命名為ResultWithValue<Value,Error>。ResultWithValue實際上就是rust的Result,而Result則對應到rust的Option(但是它包含的值是Error)。用前一篇文章的概念來實作應該是實作成抽象類別Result和兩個子類SuccessResult和ErrorResult,但這種實作方式必須限制Result只能有這兩個子類。因為c#沒有像是java sealed的語法,因此在這裡只能用internal constructor的方式取巧地限制繼承。把ResultWithValue當作Result的延伸從物件導向的角度來說的確很合理,這種設計大概是啟發自c#的Task和Task<Result>,這能有效重用兩者的程式碼。一般繼承是使乘法類別橫向發展,例如A*B的類別延伸後可以變成A*B*C,這對應到函數式編程的row polymorphism,但這裡把加法類別也用這種方法做延伸,雖然使用row polymorphism也可以做到類似的事(purescript-variant),但一般不建議這麽做。這種繼承的用法我覺得非常有趣,也顯示出物件導向與函數式編程不同的思考方式。


合理穩健的型別系統可以提供開發者巨大的救贖,上篇文章討論的代數資料類型就是其中的精髓。在缺少加法類型的程式語言裡我們只能利用null pointer表示失敗,或是使用多個變數但只有一個為非空指標來表示不同狀態下的資料。這種基於習慣的用法依賴於開發者的細心,並不斷消耗有限的專注力。透過將狀態綁定到類型上,我們可以用更精確的方式描述程式邏輯,並把背後的結構看得更清楚。提供Tuple類型可以讓開發者更習慣使用回傳值傳回結果,而非使用參數參考傳回,或是暫存在內部並提供方法取得各個數值;提供Result類型可以讓開發者更容易傳回執行狀態,而非使用特殊值表示狀況,或是乾脆忽略例外狀況。合理的類型系統在簡單的情況下不需要任何說明,只靠名稱和結構就足以描述其意義與使用方法。比起寫一堆說明文檔講解應該怎麼使用這個方法,不如使用類型系統讓你只能這麽使用。這種依靠類型系統「說話」的方式很常出現在類型驅動開發,而只有當類型系統足夠穩健才有辦法這樣玩。在合理的類型系統下開發更容易除錯,因為不對的狀態變得不可表示,因而把執行時期的錯誤提昇到編譯時期。


並不是類型越豐富,能力越強就比較好。過多過於細緻的類型系統會增加學習難度,rust就是這樣的語言,然而對於關注記憶體安全的程式語言來說,這麽做是有必要的。但是rust的語法就有點複雜且多餘,例如rust提供非常多的方法處理加法類型,而其中包含了match, if let, let else, while let等語法,而且人們似乎還覺得不夠用(if let chains)。rust因為關於生命週期的類型使得操作一些結構變得複雜繁瑣,因而引入了很多語法與一些特殊的語義以簡化寫法,卻讓規則變得特別複雜。但對於函數式程式語言,一般只有case of和pattern matching兩種語法,而這些也足以描述所有可能的狀況。我認為有限的語法更能把邏輯理清,它能讓你意識到所有流程控制都是基於加法類型的pattern matching,這是理解函數式編程的重要關鍵。有時候功能過於強大也會造成不好的結果。Java這類語言的物件可以動態地檢閱實體類別並使用其中的方法,這稱為反射機制。這種機制使得我們可以直接把Java當作動態類型的程式語言來操作。這在一些地方很方便,但也使得一些糟糕的設計更容易出現(函式的參數都是Object,但事實上只能傳入特定類別,因為它會藉由反射機制操作)。問題不在於Java能做到這點,而是任何類別的變數都能這樣做,以致於我們沒辦法判斷他何時會使用反射機制。就算拿掉反射機制,instanceof就已經過於強大,然而在rust必須使用Any trait才能做到類似的事情。另外c++的const cast也是,利用const cast可以騙過編譯器常數的約定,使得const變得沒意義。rust也有類似的東西,但它把const cast做的很複雜並包在unsafe裡,明確說明了這個方法的風險。


有紀律的類型系統不會輕易開後門,因此不可變的還是不可變,不可能知道的就是不知道,無法存取的就不應該有偷渡的方法。類型系統的紀律關係到對類型系統的信賴,沒有紀律我們就必須相信其他開發者的素養。如果為了遵守紀律而把後門管的很嚴,在處理一些特殊狀況時就會不方便或者不可能,然而通常這時要想的是這種方法是否真的合理,如果真的合理的話你應該要把它抽取出來重用,而非抱怨它很難用。rust就是刻意把功能較強、容易出錯、不安全的方法設計得比較複雜,如此一來工程師就會為了方便使用比較安全的方法實作,若是想要用容易出錯的方法實作,也會因為寫起來比較複雜的關係而傾向包裝起來重用。這種人因工程的設計是rust的一大特點。而Python和TypeScript這種在動態類型程式語言上附加的靜態類型系統就沒有紀律可言,而嚴謹也不是它們的目的。TypeScript能提升JavaScript的表達力,增加帶有語義的類型標註能讓程式邏輯更清晰,並減少出錯的機會。然而不能指望TypeScript編譯器的檢查,除非你能掌握所有程式碼,並正確地使用類型與確實避開各種陷阱。紀律的好處不只是能獲得信賴,還能免費得到一些承諾。例如看到rust函式fn what<A>(a:A) -> Vec<A>,我們有99%的信心相信他總是會回傳一個只帶有一個元素(即參數a)的列表,或總是回傳沒有元素的列表,而參數就直接丟掉了,只是一般不會這麼寫。但它不可能回傳多個元素的列表,因為它不知道如何複製參數,更不可能憑空捏出類型為A的值,除非使用unsafe。然而如果是Java的話,它不只有可能會回傳多個元素的列表(任何物件參考都可以複製),還有可能會根據參數的實體類別回傳不同的長度,而這些都是使用普通的語言特性就能實現的。這種藉由泛型推導出的承諾稱為free theorem,顧名思義就是免費的理論(theorem for free!):我們不需要付出任何代價,只需要寫下函式的類型宣告,就能免費得到承諾。只要稍微有點違規,這些承諾就無法兌現,這顯示了紀律的重要性。函數式程式語言更注重這種紀律,因此大部分的方法只要看過類型,甚至連名稱都不用看,就能知道他在幹嘛。


4會員
28Content count
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
發表第一個留言支持創作者!
have bear的沙龍 的其他內容
寫上一篇文章時我意識到,類型,類別,型別這幾個詞在物件導向當道的現代變得有些模糊,常常會不小心當成是物件導向的類,但我指的其實是資料類型。在英文中,我常常這樣區分它們:物件導向的類是class,代表的是抽象的物件模型,而類型是type/data type,代表的是實際的資料結構。正如上一篇文章所說,
承接上一篇文章,現代的物件導向已經走偏了,他就像null pointer,很容易出現不好的設計。自從我深入學習函數式編程後,漸漸發現物件導向的不合理的設計,而學習rust之後更讓我開始討厭物件導向,rust幾乎把所有我認為不好的地方都修正了。這個系列的文章我將會一一比較物件導向與rust的差異。這篇
在現代,物件導向雖然仍是主流,函數式慢慢得到關注。物件導向並不適合所有的程式邏輯,但在像是Java的物件導向的程式語言中,幾乎所有值都被當作物件,因此在一些情境下Java寫起來會非常冗余。物件導向流行的原因大概是因為它的思考方式比較符合我們對於世界的認知,但邏輯推理與解決問題的方式卻不一定符合我們的
寫上一篇文章時我意識到,類型,類別,型別這幾個詞在物件導向當道的現代變得有些模糊,常常會不小心當成是物件導向的類,但我指的其實是資料類型。在英文中,我常常這樣區分它們:物件導向的類是class,代表的是抽象的物件模型,而類型是type/data type,代表的是實際的資料結構。正如上一篇文章所說,
承接上一篇文章,現代的物件導向已經走偏了,他就像null pointer,很容易出現不好的設計。自從我深入學習函數式編程後,漸漸發現物件導向的不合理的設計,而學習rust之後更讓我開始討厭物件導向,rust幾乎把所有我認為不好的地方都修正了。這個系列的文章我將會一一比較物件導向與rust的差異。這篇
在現代,物件導向雖然仍是主流,函數式慢慢得到關注。物件導向並不適合所有的程式邏輯,但在像是Java的物件導向的程式語言中,幾乎所有值都被當作物件,因此在一些情境下Java寫起來會非常冗余。物件導向流行的原因大概是因為它的思考方式比較符合我們對於世界的認知,但邏輯推理與解決問題的方式卻不一定符合我們的
你可能也想看
Google News 追蹤
Thumbnail
本專欄將提供給您最新的市場資訊、產業研究、交易心法、優質公司介紹,以上內容並非個股分析,還請各位依據自身狀況作出交易決策。歡迎訂閱支持我,獲得相關內容,也祝您的投資之路順遂! 每年 $990 訂閱方案👉 https://reurl.cc/VNYVxZ 每月 $99 訂閱方案👉https://re
Thumbnail
kakao tv原創節目〖耳膜少年團〗,集結五位有「耳膜男友」稱號,具備現場表演、作詞、作曲能力的實力派Solo歌手們,挑戰組成全新男團的出道企劃。由Paul Kim、金珉碩、鄭承煥、夏賢尚、BIG Naughty組成的耳膜少年團,和DAZED分享他們眼中每個成員的樣子。
Thumbnail
  新世紀福音戰士的劇場版最終章有一幕是真嗣與老爸的心靈溝通,畫面中驀地出現整集故事一開始就出現的隨身聽。帶出老爸的心聲「耳機阻隔自己與外界的連結」(實際句子可能有點出入)。這讓我想到,說不定,真的說不定,整部 EVA 企劃的構思源頭,就是從耳機這個幾十年前就出現的產品開始幻想、衍伸。
Thumbnail
  故事的主角是一名中學老師檀千鄉。他能夠透過超能力看見未來,某次藉此阻止身邊的人發生慘劇之後,卻導致自己被捲入無以復加的危機之中。   整體而言《佩珀爾的幻象》節奏輕快、情節曲折、也適當地穿插幽默感、以及哲學思辨與印證,是一部追求娛樂效果的讀者能夠感到滿足、而注重深度的讀者也能夠好好享受的作品;另
Thumbnail
阿崴閱讀-薩提爾的對話練習 溝通的初衷其實不是要解決表象的問題,而是要探討人的內在。是對人不對事,這真的與傳統教養或是工作商業中大相逕庭。但是所有的事物都是需要刻意練習,或許現在沒有甚麼機會有一個好的導師可以指導,但就從保持好奇心開始吧。
Thumbnail
一篇文章、一則金句、一個小觀念大改變,透過文字啟發你的一週,幫助你持續成長,遇見更好的自己。
Thumbnail
美國革命的風吹起之後,菁英主義的統治方式也開始產生動搖,建國先賢創立的制度,讓民主制度深化到平民階層,報社的主要客群不在是紳士,以往不問世事的中產階級們也開始關心政治,民主世界從菁英制開始走向齊頭化、扁平化。 政治領袖變成了自己討厭的樣子,為了選票而去討好群眾。作者認為這是為了民主而必然付出的代價。
Thumbnail
曾經於1998年來台尋找失蹤兒子Reuben(魯本)身影的紐西蘭父親Phil Tchernegovski在2022年5月上映的紀錄片費爾的旅程(Phil’s Journey)開頭是這樣描述兒子的歸處,魯本墜入了山洞,洞裡有樹結出果實,鳥兒吃了果實又將種子撒進山洞,而魯本化成了樹,最後成為一片森林。
Thumbnail
八月的北半球熱得要命,南半球卻冷得要命,沒收入不打緊,還可以省吃儉用,但是我們不知道的是,八月的墨爾本是當地最冷的一個月,動不動就颳風下雨,連離開被窩都是痛苦的折磨。 在墨爾本歷經滄桑,最後總結出幾種找工方式:網路投履歷、打電話詢問、找仲介以及掃街。
Thumbnail
本專欄將提供給您最新的市場資訊、產業研究、交易心法、優質公司介紹,以上內容並非個股分析,還請各位依據自身狀況作出交易決策。歡迎訂閱支持我,獲得相關內容,也祝您的投資之路順遂! 每年 $990 訂閱方案👉 https://reurl.cc/VNYVxZ 每月 $99 訂閱方案👉https://re
Thumbnail
kakao tv原創節目〖耳膜少年團〗,集結五位有「耳膜男友」稱號,具備現場表演、作詞、作曲能力的實力派Solo歌手們,挑戰組成全新男團的出道企劃。由Paul Kim、金珉碩、鄭承煥、夏賢尚、BIG Naughty組成的耳膜少年團,和DAZED分享他們眼中每個成員的樣子。
Thumbnail
  新世紀福音戰士的劇場版最終章有一幕是真嗣與老爸的心靈溝通,畫面中驀地出現整集故事一開始就出現的隨身聽。帶出老爸的心聲「耳機阻隔自己與外界的連結」(實際句子可能有點出入)。這讓我想到,說不定,真的說不定,整部 EVA 企劃的構思源頭,就是從耳機這個幾十年前就出現的產品開始幻想、衍伸。
Thumbnail
  故事的主角是一名中學老師檀千鄉。他能夠透過超能力看見未來,某次藉此阻止身邊的人發生慘劇之後,卻導致自己被捲入無以復加的危機之中。   整體而言《佩珀爾的幻象》節奏輕快、情節曲折、也適當地穿插幽默感、以及哲學思辨與印證,是一部追求娛樂效果的讀者能夠感到滿足、而注重深度的讀者也能夠好好享受的作品;另
Thumbnail
阿崴閱讀-薩提爾的對話練習 溝通的初衷其實不是要解決表象的問題,而是要探討人的內在。是對人不對事,這真的與傳統教養或是工作商業中大相逕庭。但是所有的事物都是需要刻意練習,或許現在沒有甚麼機會有一個好的導師可以指導,但就從保持好奇心開始吧。
Thumbnail
一篇文章、一則金句、一個小觀念大改變,透過文字啟發你的一週,幫助你持續成長,遇見更好的自己。
Thumbnail
美國革命的風吹起之後,菁英主義的統治方式也開始產生動搖,建國先賢創立的制度,讓民主制度深化到平民階層,報社的主要客群不在是紳士,以往不問世事的中產階級們也開始關心政治,民主世界從菁英制開始走向齊頭化、扁平化。 政治領袖變成了自己討厭的樣子,為了選票而去討好群眾。作者認為這是為了民主而必然付出的代價。
Thumbnail
曾經於1998年來台尋找失蹤兒子Reuben(魯本)身影的紐西蘭父親Phil Tchernegovski在2022年5月上映的紀錄片費爾的旅程(Phil’s Journey)開頭是這樣描述兒子的歸處,魯本墜入了山洞,洞裡有樹結出果實,鳥兒吃了果實又將種子撒進山洞,而魯本化成了樹,最後成為一片森林。
Thumbnail
八月的北半球熱得要命,南半球卻冷得要命,沒收入不打緊,還可以省吃儉用,但是我們不知道的是,八月的墨爾本是當地最冷的一個月,動不動就颳風下雨,連離開被窩都是痛苦的折磨。 在墨爾本歷經滄桑,最後總結出幾種找工方式:網路投履歷、打電話詢問、找仲介以及掃街。