同調的泛型

更新於 2024/01/06閱讀時間約 7 分鐘

前幾篇文章討論了類型系統的合理性,而這會影響我們對於變數與函式是什麼的理解。其中泛型是當中很重要的一個元素,很多討論都是基於泛型的使用。泛型會大大地增加類型系統的複雜度,因此有些語言選擇不提供泛型(go),但缺少泛型又會使簡單的容器都無法用類型精確描述。泛型的強大必須結合有紀律的類型系統才能顯現,但不能做動態類型檢查的類型系統必須有泛型,否則就連最簡單的抽象化都做不到。這顯示了泛型本質上與抽象化緊密相連,而且使用抽象化的唯一方法就是泛型。在這種類型系統,類型描述的是實際的資料結構,而抽象化的任務全部都交給泛型完成,不像物件導向類別同時可以描述抽象的物件。


「抽象化」是將某些結構或概念從實際案例中抽取出來,並讓它可以應用在未知的情境下,而泛型就是量化未知的方法。例如我們有 fn swapIntAndString(Tuple<int,string>)->Tuple<string,int> 和 fn swapFloatAndBool(Tuple<float,bool>)->Tuple<bool,float>等函式,即使它們的類型都不一樣,但實作看起來都一樣。它們都是把tuple的兩個元素做交換,因此可以將這種結構抽取成泛型函式。就像把相同的程式碼抽取出來時,需要將不同的部分改寫成參數,抽取泛型函式時也需要將不同的類型改寫成類型變數。例如:fn swap<T,S>(Tuple<T,S>)->Tuple<S,T>,其中T和S是類型參數。抽取函數可以把相同的運算邏輯獨立出來,而使用泛型可以進一步把無關的資訊排除,例如交換Tuple的元素不需要知道內容是什麼,因此我們可以把這部分的資訊取代成一個佔位符。在像是c, go這種沒有泛型的程式語言是做不到這種抽象化的,只能藉由類型轉換自己判斷應該是什麼。


泛型函式需要輸入變數與類型作為參數,而參數的類型跟類型參數通常都是有關的,因此編譯器大多時候都能自動推斷類型參數。這似乎代表類型與變數是同等的存在,但事實上在大多數靜態類型程式語言不是如此,通常類型只會出現在編譯時期,類型的值不會受到變數影響,這代表類型與變數是兩個世界。參數可以看作表達式的佔位符,因此類型參數也能看作函式類型表達式的佔位符,而像是Tuple<A,B>, fn(A)->B等類型建構子則可以看作類型的函式:它們接受兩個類型並回傳類型。這種函式和一般的函式還是差了一大截,因為F<A,B>在所有類型之中都是唯一的,不可能有其他的表達式能表示相同的類型,這就是為什麼他稱做類型建構子而非類型函式(在typescript中則真的是一種函式了)。這種特性使得類型看起來是什麼就是什麼,而泛型真的就像是類型的範本一樣一目了然。


如果函式的操作與變數的類型或是資料結構的某個部分類型無關,那麼這個類型就能抽取出來變成類型變數。就算你的情境並不需要這麼做,也應該要嘗試使用泛型,因為這麼做可以表示這部分的邏輯與這個類型無關。相較於直接把類型取代成void pointer/empty interface/any type,泛型把未知的類型參數化,使得變數之間的聯繫變得清楚,更能描述整體結構,這對容器相關的資料結構特別重要。當泛型函式關係到多個未知的類型時,類型變數可以把相關的未知類型形成同調,讓不同的「未知」能夠跨越多個函式而不會混在一起。這就像原本作為範本上的「洞」的類型變數,跨越整個計算過程形成「隧道」,而這些隧道編織出的形狀描述了這個計算過程的整體結構。透過free theorem,我們甚至能直接推斷出這些隧道大概長怎樣。


物件導向用子類關係把無關的資訊隱藏起來,而泛型透過參數化把無關的資訊抽離,它們的差別在於抹除資訊的方法,泛型的做法保留了同調性。利用同樣的概念可以把物件導向的繼承機制用泛型模擬出來。PureScript擁有一種無序的乘法資料結構Record,這種結構能夠擴展成員變數,這很像TypeScript的Record,但他具有更健全的語義。例如 { a :: String, b :: Boolean } 代表一個帶有a, b成員的物件,但不能有其他成員,這一點跟TypeScript很不一樣。TypeScript的Record只告訴你可以存取哪些成員,但它可能有其他成員。從物件導向的角度來看,擁有其他成員的物件可以看作這個類型的子類,也就是它同時包含了這個類型的物件實體和其子類的物件實體,因此他更像抽象類別/介面而非資料類型。PureScript的Record類型明確描述所有的成員,若要像TypeScript只關注某些成員的存取,可以透過泛型把未知的部分參數化(row polymorphism),寫成 { a :: String, b :: Boolean | r },其中r是特殊的類型變數,它代表所有其他的成員。這與TypeScript的差別在於它不會丟掉任何資訊,例如函式 increase :: { counter :: Number | r } -> { counter :: Number | r },使用這個函式不會丟任何類型資訊。在TypeScript要達到相同效果必須使用帶有約束的泛型函式 <T extends {id:Number}>(val: T) => T。反之,在PureScript要像TypeScript丟掉類型資訊就必須使用quantifier,寫成 exist r. { counter :: Number | r }(事實上PureScript沒有exist,但可以使用forall做出來)。


定義函式時應盡量不要丟掉類型資訊,因為這樣很容易忽略未考慮的狀況,而類別的方法一般都是在這樣的情況下定義的函式,因此應該少用方法。例如在為類別定義相等方法時一般寫作:equals(other: ThisClass): boolean,然而當遇到繼承它的子類物件實體時該怎麼辦?很明顯子類的新成員變數也應該一起比較,而且比較不同類別的實體是沒有意義的,因此這個方法本質上就是不可繼承的。若是寫成泛型函式問題就很明顯:function equals<T extends ThisClass>(a: T, b: T): boolean,這個函式看起來就不可能實作,因為你不知道類型T是什麼是要怎麼比較。如果你真的把所有子類的判斷都寫死在裡面,就違反了依賴抽象的規則,這很容易出錯,除非你能確定不會有其他類別繼承ThisClass,不過這時你應該嘗試使用加法類型(至少在TypeScript是可以這麼做的)。物件導向的變數類型宣告預設會丟掉實體類型的資訊,使得在實作時必須使用動態型別檢查(或是自己檢查),這讓類型資訊不能順暢地流動,從而產生更多的錯誤機會。而使用泛型雖然會讓變數變多,因而產生太多雜音影響清晰度,但類型得以串聯起來,讓編譯器能為你檢查錯誤。物件導向傾向在執行時期使用類型,而函數式編程一般只能在編譯時期使用類型,好處是這麼做可以在編譯時期就檢查出錯誤。然而有時仍需要丟掉類型資訊,或是把類型資訊下放到執行時期(為了減少雜音或是聯集不同類型),而這對大部分函數式程式語言來說不可能做到或是很難用。例如rust提供了trait object把類型資訊丟掉,並提供Any trait在執行時期存取類型資訊;Haskell或是PureScript可以使用forall把類型資訊隱藏起來,但沒辦法在執行時期存取類型資訊,Idris2則可以使用Sigma type做到。事實上並不常需要丟掉類型資訊(在物件導向常常用到只是因為它的設計比較容易這麼使用),而且大多時候仍可以透過自己寫加法類型手動把類型資訊帶到執行時期。

avatar-img
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
have bear的沙龍 的其他內容
函式跟資料結構一樣都有類型,它不只是特定於函式的概念,而是跟int, tuple<bool,float>等類型同等的概念。在c++函式的類型可以寫做如std::function<int(float,float)>,它可以放在tuple, array等容器裡,當然也可以作為函式的參數或是傳回值,如st
前幾篇文章在討論類型時,只討論了乘法與加法類型,這只是最基礎的類型構造方式,另外還有函式類型和泛型等概念還沒討論。在討論函式的類型之前,必須先討論函式的正確用法。對於程序式編程來說,函式是一段可重複使用的執行代碼,輸入的參數是用來控制執行行為的,因此比起函式(function)更應該稱它為程序(pr
物件導向程式語言的類型系統總是不合理的,這些程式語言承襲至舊的語言不好的特性,而人們並沒有意識到它的問題,或者比起健全(soundness)它們更注重熟悉(familiarity)。多數的物件導向程式語言都源自c/c++,而c++有太多糟糕的設計,然而同一時期出來的Haskell卻很少有類似的問題。
寫上一篇文章時我意識到,類型,類別,型別這幾個詞在物件導向當道的現代變得有些模糊,常常會不小心當成是物件導向的類,但我指的其實是資料類型。在英文中,我常常這樣區分它們:物件導向的類是class,代表的是抽象的物件模型,而類型是type/data type,代表的是實際的資料結構。正如上一篇文章所說,
承接上一篇文章,現代的物件導向已經走偏了,他就像null pointer,很容易出現不好的設計。自從我深入學習函數式編程後,漸漸發現物件導向的不合理的設計,而學習rust之後更讓我開始討厭物件導向,rust幾乎把所有我認為不好的地方都修正了。這個系列的文章我將會一一比較物件導向與rust的差異。這篇
在現代,物件導向雖然仍是主流,函數式慢慢得到關注。物件導向並不適合所有的程式邏輯,但在像是Java的物件導向的程式語言中,幾乎所有值都被當作物件,因此在一些情境下Java寫起來會非常冗余。物件導向流行的原因大概是因為它的思考方式比較符合我們對於世界的認知,但邏輯推理與解決問題的方式卻不一定符合我們的
函式跟資料結構一樣都有類型,它不只是特定於函式的概念,而是跟int, tuple<bool,float>等類型同等的概念。在c++函式的類型可以寫做如std::function<int(float,float)>,它可以放在tuple, array等容器裡,當然也可以作為函式的參數或是傳回值,如st
前幾篇文章在討論類型時,只討論了乘法與加法類型,這只是最基礎的類型構造方式,另外還有函式類型和泛型等概念還沒討論。在討論函式的類型之前,必須先討論函式的正確用法。對於程序式編程來說,函式是一段可重複使用的執行代碼,輸入的參數是用來控制執行行為的,因此比起函式(function)更應該稱它為程序(pr
物件導向程式語言的類型系統總是不合理的,這些程式語言承襲至舊的語言不好的特性,而人們並沒有意識到它的問題,或者比起健全(soundness)它們更注重熟悉(familiarity)。多數的物件導向程式語言都源自c/c++,而c++有太多糟糕的設計,然而同一時期出來的Haskell卻很少有類似的問題。
寫上一篇文章時我意識到,類型,類別,型別這幾個詞在物件導向當道的現代變得有些模糊,常常會不小心當成是物件導向的類,但我指的其實是資料類型。在英文中,我常常這樣區分它們:物件導向的類是class,代表的是抽象的物件模型,而類型是type/data type,代表的是實際的資料結構。正如上一篇文章所說,
承接上一篇文章,現代的物件導向已經走偏了,他就像null pointer,很容易出現不好的設計。自從我深入學習函數式編程後,漸漸發現物件導向的不合理的設計,而學習rust之後更讓我開始討厭物件導向,rust幾乎把所有我認為不好的地方都修正了。這個系列的文章我將會一一比較物件導向與rust的差異。這篇
在現代,物件導向雖然仍是主流,函數式慢慢得到關注。物件導向並不適合所有的程式邏輯,但在像是Java的物件導向的程式語言中,幾乎所有值都被當作物件,因此在一些情境下Java寫起來會非常冗余。物件導向流行的原因大概是因為它的思考方式比較符合我們對於世界的認知,但邏輯推理與解決問題的方式卻不一定符合我們的
你可能也想看
Google News 追蹤
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Thumbnail
你家的孩子, 情緒屬於<刺蝟型>?還是屬於<悶葫蘆型>? 相信無論是哪一型, 對於家長都充滿了挑戰, 對吧 ! 你說這有沒有解套的方法呀 ? 有的 ! 帶孩子一起透過遊戲化的情境, 帶孩子學習兒童SEL吧 !
Thumbnail
 #延續母乳保護力、調整體質的兒童營養好物-孕哺兒日本乳鐵蛋白 https://momtobe.cc/cing9 面臨家有幼兒園、從小體質敏感又常常不舒服請假的孩子著實讓我頭痛萬分,但自從讓晴晴持續補充了孕哺兒的 #兒童營養品 後,漸漸的她的體質也慢慢的改變了,這讓我更信賴孕哺兒了。 #孕哺兒
Thumbnail
在諮商關係的角色互動裡,我的塑造比例與先後是: 1.建立關係與同理需要「同調」 2.從「互補」面找新的契機 3.整合進去,讓案主有「新的平衡」
Thumbnail
「一年中會有一兩天,天空下起份量極重的雨水,因為只有那一兩天,地球的角度剛好讓冷空氣和熱對流形成一道水門,只要偏差一點,水門就不存在。」~黃國峻《 水門的洞口》
Thumbnail
「今天大家很開心的分享昨晚帶回家的水餃,同學都說好吃、很開心,而Chace失落的說阿嬤把水餃丟掉了。」 聽完老師這麼說,我才驚覺Chace這麼放在心上,並且是失落的,但卻沒與我跟阿嬤表達。 當晚回家後,我立馬跟阿嬤說這件事,阿嬤也很不好意思的跟Chace道歉,並說明昨晚帶回家,Chace沒有即時說有
Thumbnail
當每件事都是妳真心渴望想做的,妳自然就會抽出時間去做。比起管理時間,更重要的是找到人生中的北極星,確立方向後,就知道該怎麼走。清單上那些超出負荷的待辦事項,只是過度努力的證明,而不是做事多有效率。 仔細想想,確實是如此。就像寫作這件事,因為是真心渴望想做的,就算再忙也會抽出時間寫作,透過文字重新梳理
在美國每年約有 5 百萬人罹患皮膚癌,其中以基底細胞癌( Basal cell carcinoma,BCC )和鱗狀細胞癌( Squamous cell carcinoma,SCC )居多,而黑色素瘤( melanoma )雖只佔 1%的比例,但致死率極高。
十八歲打死都不願吃維A的發雞盲少女專門跟母親對著幹。13號瞪著叫伊琳的少女。 13號:「你想把母親活活的氣死還是真心的跟自己雙眼過不去呢?」 伊琳:「⋯⋯她說今天要去跟她丈夫簽離婚協議書嘛!我不是已經坐在你面前聽帥哥您的辱駡嘛!」 13號:「那你到底有沒有吃我上次給你的維生素呢?我的小美女。」 伊琳
Thumbnail
如果我們認同「懺悔」是一種崇高人性的展現,我們就不該否定「傑克蓋的房子」有其正面意義,更何況這還不是那種聲淚俱下、叫苦連天似的懺悔,而是唱作俱佳、普天同慶的懺悔,150分鐘,拉斯馮提爾教給了我們,原來還有這種形式的懺悔。
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Thumbnail
你家的孩子, 情緒屬於<刺蝟型>?還是屬於<悶葫蘆型>? 相信無論是哪一型, 對於家長都充滿了挑戰, 對吧 ! 你說這有沒有解套的方法呀 ? 有的 ! 帶孩子一起透過遊戲化的情境, 帶孩子學習兒童SEL吧 !
Thumbnail
 #延續母乳保護力、調整體質的兒童營養好物-孕哺兒日本乳鐵蛋白 https://momtobe.cc/cing9 面臨家有幼兒園、從小體質敏感又常常不舒服請假的孩子著實讓我頭痛萬分,但自從讓晴晴持續補充了孕哺兒的 #兒童營養品 後,漸漸的她的體質也慢慢的改變了,這讓我更信賴孕哺兒了。 #孕哺兒
Thumbnail
在諮商關係的角色互動裡,我的塑造比例與先後是: 1.建立關係與同理需要「同調」 2.從「互補」面找新的契機 3.整合進去,讓案主有「新的平衡」
Thumbnail
「一年中會有一兩天,天空下起份量極重的雨水,因為只有那一兩天,地球的角度剛好讓冷空氣和熱對流形成一道水門,只要偏差一點,水門就不存在。」~黃國峻《 水門的洞口》
Thumbnail
「今天大家很開心的分享昨晚帶回家的水餃,同學都說好吃、很開心,而Chace失落的說阿嬤把水餃丟掉了。」 聽完老師這麼說,我才驚覺Chace這麼放在心上,並且是失落的,但卻沒與我跟阿嬤表達。 當晚回家後,我立馬跟阿嬤說這件事,阿嬤也很不好意思的跟Chace道歉,並說明昨晚帶回家,Chace沒有即時說有
Thumbnail
當每件事都是妳真心渴望想做的,妳自然就會抽出時間去做。比起管理時間,更重要的是找到人生中的北極星,確立方向後,就知道該怎麼走。清單上那些超出負荷的待辦事項,只是過度努力的證明,而不是做事多有效率。 仔細想想,確實是如此。就像寫作這件事,因為是真心渴望想做的,就算再忙也會抽出時間寫作,透過文字重新梳理
在美國每年約有 5 百萬人罹患皮膚癌,其中以基底細胞癌( Basal cell carcinoma,BCC )和鱗狀細胞癌( Squamous cell carcinoma,SCC )居多,而黑色素瘤( melanoma )雖只佔 1%的比例,但致死率極高。
十八歲打死都不願吃維A的發雞盲少女專門跟母親對著幹。13號瞪著叫伊琳的少女。 13號:「你想把母親活活的氣死還是真心的跟自己雙眼過不去呢?」 伊琳:「⋯⋯她說今天要去跟她丈夫簽離婚協議書嘛!我不是已經坐在你面前聽帥哥您的辱駡嘛!」 13號:「那你到底有沒有吃我上次給你的維生素呢?我的小美女。」 伊琳
Thumbnail
如果我們認同「懺悔」是一種崇高人性的展現,我們就不該否定「傑克蓋的房子」有其正面意義,更何況這還不是那種聲淚俱下、叫苦連天似的懺悔,而是唱作俱佳、普天同慶的懺悔,150分鐘,拉斯馮提爾教給了我們,原來還有這種形式的懺悔。