多型的差異

更新於 發佈於 閱讀時間約 8 分鐘

所謂的多型是讓一個函式或是資料結構能擁有多個不同的類型,其中上一篇文章所談的就是參數多型(parametric polymorphism),這篇文章將繼續討論特設多型(ad hoc polymorphism)。特設多型跟泛型的差別在於:泛型函式對於所有的類型只能有一種實作,而特設多型會根據類型有不同的實作。物件導向就是藉由改寫方法的機制實現特設多型,另外一種方法是藉由實作介面。特設多型的所有可能的實作不需要放在同一個地方,你可以在其他檔案/模組/專案寫下它對特定類型的實作,而在使用時(大部分的)編譯器會為你找出最合適的實作。這個特性使得它能用來表達抽象的概念,而非特定的函式實作,這樣才能讓程式碼具有可延展性。


多型能讓一個名詞囊括一類特性,使它們凝聚成一種抽象概念,缺乏多型的能力會讓程式碼與概念的重用性降低。當我們要為不同的類型實作某種共通的抽象操作時,我們必須使用多型。例如fn swap<A,B>(Tuple<A,B>)->Tuple<B,A>就是將所有交換Tuple元素的操作抽象化的多型函式。但並不是所有的概念都能用同一種實作描述,只有泛型是不夠用的。例如fn toString<A>(A) -> String是將變數轉換成文字以供閱讀的函式,對於每個類型都有不同的實作,這時就需要使用特設多型為每個類型實作不同的操作。特設多型讓我們能夠在不知道實際類型的情況下做出不同的行為。例如,我們沒辦法直接實作出fn sort<T>(Array<T>) -> Array<T>,因為我們不知道如何比較未知的類型T,我們至少需要知道如何比較T,因此需要給類型參數一個約束T: Ord,這代表存在函式fn compare(T, T) -> Ordering的實作,因此排序函式可以利用這個函式完成操作。如果沒有這個約束,所有類型的列表丟進去都會受到一樣的交換,如果sort({1,2,3}) == {2,3,1},那麽sort({'a','s','d'})就會得到{'s','d','a'}。物件導向的程式語言則可以透過檢查實體類型做出不同的行為,但這只能針對已知的類別進行差別處理,這並不能用來實現sort這種需要你不知道的類型的某種特性的函式。


「特設多型」的定義是指函式根據類型有不同的行為,然而這並不是最重要的功能。特設多型函式還允許讓不同類型的實作放在不同地方,你甚至可以在其他檔案/模組/專案寫下它對特定類型的實作,這使得這個函式可以被延展。因此使用(未實例化的)特設多型函式時是不可能知道它實際會怎麼運作,這強迫我們必須以抽象的方式思考。這種延展性與物件導向的介面實作機制類似,它能讓一個抽象概念(參數化類型)在不同地方被明確描述(被實作)。就如上面所提到的,像是sort這種需要非特定類型的特性的情境,大部分時候這只能依靠特設多型的可延展性實現,而這才是特設多型的最大用處。


物件導向可以藉由介面實現特設多型,它將這種函式放在一個類型名下,並給予此名稱一個概念,而定義類別時可以透過實作此介面附加針對此類型的實現。在TypeScript寫成 interface Display { toString(): String; },其中物件本身是隱含在toString的一個泛型變數。然而對於物件導向來說,這個方法是屬於物件的而非附加給Display的,所謂的「介面」只是為了介接給其他人使用的一個「特殊類別」,它並不是獨立於類別的存在。這種觀點使得介面只能在定義類別時實作,若是你新定義了一個介面想讓已有的類別實作,就必須回頭去改寫類別的實作,這樣會讓類別的定義包含各種抽象方法的實作,使得概念與關注點沒辦法集中在個別的模組內。物件導向的這種設計迫使我們必須把關注點與物件綁在一起,在這種情況下通常會將這些想要抽離的實作再分出一個類別,並以組合的方式合併。相對地,rust的trait用法類似於介面,它底下的方法被稱為關聯方法而非成員方法,因為它並不屬於此類型。為特定類型實作trait的程式碼不是寫在定義類型時,在一些情況下也可以在跟類型不同的模組實作trait,甚至可以為符合某種條件的類型實作trait。因此trait應被視為獨立於類型的存在,它是用來描述這個類型帶有什麼樣的特性,而所謂的特性並不是資料,因此不應跟用來描述資料的類型綁在一起。rust也允許把方法綁定到類型名稱上,它只代表這些方法與此類型有很緊密的關係,但它不具有繼承的特性,所有繼承能實現的功能都應交由trait完成,因為這是trait的職責。


雖然rust的trait system用起來很像介面,但它其實來自於Haskell的typeclass,因此有些特性用介面的角度來看很神奇。rust的trait寫成類似於介面的語法,例如 where A: Add<B>代表A擁有加上B的特性,它更適合寫成addImpl: Add<A, B>,也就是addImpl是描述”A加上B”這個特性的物件,但這個物件是由編譯器在編譯時期幫你建構的。在Haskell則是寫成Add a b => …,這個寫法就好像Add a b是某種參數,但它也是由編譯器幫你填上的,因此並不需要煩惱這點。trait的實作代表的是一種描述某種類型的特性的函式集,而這套函式集會綁定到當前的情境,因此不需明確地說明你是要使用哪種trait的函式。而且一般來說是不會遇到同時使用同一個trait但多種實作的情況,因此Haskell和rust都沒有給trait/typeclass參數一個名稱(PureScript可以,但沒有用途),也不能為同樣的類型實作兩個版本,Haskell甚至要求typeclass必須能唯一地決定。rust跟Haskell的這些機制都是發生在編譯時期,因此實際上並沒有addImpl這個物件,前面提的這些看法只是我的想像,事實上可能不是這麼運作的,但ocaml真的把trait的實作當作是函式集的物件。ocaml把類型與相關函式包成一個模組,就像一個struct一樣,而這個模組也有類型。它的類型包含了定義的類型名稱、定義的變數類型和函式類型,其中它們的類型可以跟它自己定義的類型有關,具有相同類型結構的模組都被當作是同樣類型的。若要讓某個類型具有可以比較的特性,只要定義有相應結構的模組就行了,到時如果某個函式要用到這個特性,就把這個模組傳給他。例如你可以為類型mytype定義一個模組module mytypeComparable: sig type t = mytype; val compare: t -> t -> int; end,若要為mytype list做排序時,只要把他丟給sort: (module M: Comparable with type t = 't) -> 't list -> 't list就行了。


這種只看結構的類型稱做鴨子型別(duck type),我們不需要明說它實作了哪個特性,使用上會直接假設它符合我們的要求,這種規則跟直接傳入函式幾乎是一樣的:sortBy: ('t -> 't -> int) -> 't list -> 't list。使用鴨子型別會產生隱藏的依賴關係,當你為了排序而實作一個模組時,沒人知道知道這個模組描述什麼概念,直到把它傳入sort函式。TypeScript的介面也是鴨子型別,定義的名稱實際上只是別名,但language server還是會記得你使用的介面是什麼名稱,以方便在編輯時提供資訊。為資料類型或是特性取唯一的名字不只是為了方便,最主要是為了讓他們擁有明確的意義,而非只是看起來像什麼。同樣是類型為t -> t -> int的函式,如果沒有說明,很難知道他是做什麼的。而把它包裝成名為Comparable的特性,很快就能知道他是用來比較順序的,如果還不知道可以去看Comparable的說明文件。包裝成特性還能讓它應該符合哪些約束變得更明瞭,而這些約束是組成特性很重要的部分,或者說只有當這些約束存在才能形成特性本身。


留言
avatar-img
留言分享你的想法!
avatar-img
have bear的沙龍
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
have bear的沙龍的其他內容
2024/02/15
這個系列的文章主要專注於物件導向到函數式編程的差異與分析,並針對概念與機制上的不同進行比較。很多人說物件導向和函數式編程沒有哪個比較好的問題,只有哪個比較適合的問題,然而我並不這麼認為,我透過這一系列的文章從各個角度討論它們之間的優缺點就是為了闡述我的觀點。物件導向錯在沒有理論基礎,但它贏在熟悉性,
2024/02/15
這個系列的文章主要專注於物件導向到函數式編程的差異與分析,並針對概念與機制上的不同進行比較。很多人說物件導向和函數式編程沒有哪個比較好的問題,只有哪個比較適合的問題,然而我並不這麼認為,我透過這一系列的文章從各個角度討論它們之間的優缺點就是為了闡述我的觀點。物件導向錯在沒有理論基礎,但它贏在熟悉性,
2024/02/08
前一篇文章中所提的函數式的三個機制明確說明了它關注的規則與能力具體是什麼。然而這套對於函數式編程的定義主要基於特定的類型系統,作為一個編程範式來說過於狹隘(物件導向的定義也是這樣)。更廣義的,我認為函數式編程主要依循三個原則,它們可以應用於任何程式語言,就算沒有靜態類型系統的支援也可以。例如在不管類
2024/02/08
前一篇文章中所提的函數式的三個機制明確說明了它關注的規則與能力具體是什麼。然而這套對於函數式編程的定義主要基於特定的類型系統,作為一個編程範式來說過於狹隘(物件導向的定義也是這樣)。更廣義的,我認為函數式編程主要依循三個原則,它們可以應用於任何程式語言,就算沒有靜態類型系統的支援也可以。例如在不管類
2024/02/04
前面談了那麽多函數式編程與物件導向的差異,但我們還沒定義函數式編程。就像物件導向,函數式編程沒有明確的定義,每個人對於什麼是函數式編程都有不同的看法。在這裡我會總結前面的討論,給出我對於函數式編程的觀點。 物件導向注重封裝與延展性,因此一般基於三個機制:繼承、多型、封裝,它們代表了物件導向所重
2024/02/04
前面談了那麽多函數式編程與物件導向的差異,但我們還沒定義函數式編程。就像物件導向,函數式編程沒有明確的定義,每個人對於什麼是函數式編程都有不同的看法。在這裡我會總結前面的討論,給出我對於函數式編程的觀點。 物件導向注重封裝與延展性,因此一般基於三個機制:繼承、多型、封裝,它們代表了物件導向所重
看更多
你可能也想看
Thumbnail
透過蝦皮分潤計畫,輕鬆賺取零用金!本文分享5-6月實測心得,包含數據流程、實際收入、平臺優點及注意事項,並推薦高分潤商品,教你如何運用空閒時間創造被動收入。
Thumbnail
透過蝦皮分潤計畫,輕鬆賺取零用金!本文分享5-6月實測心得,包含數據流程、實際收入、平臺優點及注意事項,並推薦高分潤商品,教你如何運用空閒時間創造被動收入。
Thumbnail
單身的人有些會養寵物,而我養植物。畢竟寵物離世會傷心,植物沒養好再接再厲就好了~(笑)
Thumbnail
單身的人有些會養寵物,而我養植物。畢竟寵物離世會傷心,植物沒養好再接再厲就好了~(笑)
Thumbnail
不知你有沒有過這種經驗?衛生紙只剩最後一包、洗衣精倒不出來,或電池突然沒電。這次一次補貨,從電池、衛生紙到洗衣精,還順便分享使用心得。更棒的是,搭配蝦皮分潤計畫,愛用品不僅自己用得安心,分享給朋友還能賺回饋。立即使用推薦碼 X5Q344E,輕鬆上手,隨時隨地賺取分潤!
Thumbnail
不知你有沒有過這種經驗?衛生紙只剩最後一包、洗衣精倒不出來,或電池突然沒電。這次一次補貨,從電池、衛生紙到洗衣精,還順便分享使用心得。更棒的是,搭配蝦皮分潤計畫,愛用品不僅自己用得安心,分享給朋友還能賺回饋。立即使用推薦碼 X5Q344E,輕鬆上手,隨時隨地賺取分潤!
Thumbnail
身為一個典型的社畜,上班時間被會議、進度、KPI 塞得滿滿,下班後只想要找一個能夠安靜喘口氣的小角落。對我來說,畫畫就是那個屬於自己的小樹洞。無論是胡亂塗鴉,還是慢慢描繪喜歡的插畫人物,那個專注在筆觸和色彩的過程,就像在幫心靈按摩一樣,讓緊繃的神經慢慢鬆開。
Thumbnail
身為一個典型的社畜,上班時間被會議、進度、KPI 塞得滿滿,下班後只想要找一個能夠安靜喘口氣的小角落。對我來說,畫畫就是那個屬於自己的小樹洞。無論是胡亂塗鴉,還是慢慢描繪喜歡的插畫人物,那個專注在筆觸和色彩的過程,就像在幫心靈按摩一樣,讓緊繃的神經慢慢鬆開。
Thumbnail
婆娑無邊的太平洋,是我們自由的土地。 溫暖的陽光照耀著,照耀著高山和田園。 我們這裡有勇敢的人民,篳路藍縷,以啟山林; 我們這裡有無窮的生命,水牛,稻米,香蕉,玉蘭花。───〈美麗島〉
Thumbnail
婆娑無邊的太平洋,是我們自由的土地。 溫暖的陽光照耀著,照耀著高山和田園。 我們這裡有勇敢的人民,篳路藍縷,以啟山林; 我們這裡有無窮的生命,水牛,稻米,香蕉,玉蘭花。───〈美麗島〉
Thumbnail
什麼是 Trianglify Generator? 對於設計師來說,創造吸引人的背景圖案是日常工作的一部分。而在這其中,多邊形風格的圖片素材常常受到青睞。今天要介紹的是一個簡單又方便的解決方案——Trianglify Generator。這是一個線上工具,不需要下載和安裝軟體,只需打開瀏覽器即可使
Thumbnail
什麼是 Trianglify Generator? 對於設計師來說,創造吸引人的背景圖案是日常工作的一部分。而在這其中,多邊形風格的圖片素材常常受到青睞。今天要介紹的是一個簡單又方便的解決方案——Trianglify Generator。這是一個線上工具,不需要下載和安裝軟體,只需打開瀏覽器即可使
Thumbnail
此章節旨在介紹Java程式語言中的各種資料型別,包括基本型別、引用型別、集合型別、陣列型別、字典型別等。它還講解了如何在Java中進行型別轉換和自定義型別,並提供了相關的程式碼示例。
Thumbnail
此章節旨在介紹Java程式語言中的各種資料型別,包括基本型別、引用型別、集合型別、陣列型別、字典型別等。它還講解了如何在Java中進行型別轉換和自定義型別,並提供了相關的程式碼示例。
Thumbnail
本章節旨在介紹 TypeScript 的基本資料型別,包括內建型別、型別轉換、自訂型別、元組、集合、陣列、和字典型別。透過理解和使用這些型別,可以提高代碼的可讀性和可維護性。
Thumbnail
本章節旨在介紹 TypeScript 的基本資料型別,包括內建型別、型別轉換、自訂型別、元組、集合、陣列、和字典型別。透過理解和使用這些型別,可以提高代碼的可讀性和可維護性。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News