簡體   English   中英

在Haskell中解釋類型類

[英]Explain Type Classes in Haskell

我是一名C ++ / Java程序員,我在日常編程中使用的主要范例是OOP。 在某些線程中,我讀到了一個注釋,Type類本質上比OOP更直觀。 有人能用簡單的單詞解釋類型類的概念,這樣像我這樣的OOP人能理解嗎?

首先,我總是非常懷疑這個或那個程序結構更直觀的說法。 編程是違反直覺的,並且總是因為人們自然而然地根據具體情況而不是一般規則來思考。 改變這一點需要培訓和實踐,也稱為“學習編程”。

繼續討論問題,OO類和Haskell類型類之間的關鍵區別在於,在OO中,類(甚至是接口類)既是新類型的類型,也是新類型(后代)的模板。 在Haskell中,類型類只是新類型的模板。 更准確地說,類型類描述了一組共享公共接口的類型,但它本身並不是一種類型

所以類型類“Num”用加法,減法和乘法運算符描述數值類型。 “Integer”類型是“Num”的實例,這意味着Integer是實現這些運算符的類型集的成員。

所以我可以用這種類型寫一個求和函數:

sum :: Num a => [a] -> a

“=>”運算符左側的位表示“sum”適用於任何類型“a”,它是Num的一個實例。 右邊的位表示它采用類型“a”的值列表,並返回單個值“a”的值作為結果。 因此,您可以使用它來匯總整數列表或雙打列表或復雜列表,因為它們都是“Num”的實例。 “sum”的實現當然會使用“+”運算符,這就是你需要“Num”類型約束的原因。

但是你不能寫這個:

sum :: [Num] -> Num

因為“Num”不是一種類型。

類型和類型類之間的這種區別是我們不討論Haskell中類型的繼承和后代的原因。 一種繼承的類型類:你可以聲明一個類型類為另一個后裔。 這里的后代描述了父級描述的類型的子集。

所有這一切的一個重要結果是你不能在Haskell中擁有異類列表。 在“sum”示例中,您可以傳遞整數列表或雙精度列表,但不能在同一列表中混合使用雙精度數和整數。 這看起來像一個棘手的限制; 您如何實施舊的“汽車和卡車都是兩種類型的車輛”的例子? 根據您實際嘗試解決的問題,有幾個答案,但一般原則是您使用第一類函數顯式執行間接,而不是隱式使用虛函數。

嗯,簡短的版本是: 類型類是Haskell用於ad-hoc多態的。

......但這可能沒有為你澄清任何事情。

對於來自OOP背景的人來說,多態性應該是一個熟悉的概念。 然而,這里的關鍵點是參數ad-hoc多態之間的區別。

參數多態是指對結構類型進行操作的函數,結構類型本身由其他類型(例如值列表)參數化。 參數多態在Haskell中幾乎都是常態; C#和Java稱之為“泛型” 基本上,無論類型參數是什么,泛型函數對特定結構都做同樣的事情。

另一方面, Ad-hoc多態性意味着不同功能的集合,根據類型執行不同的 (但概念上相關的)事物。 與參數多態不同,需要為可以使用的每種可能類型單獨指定ad-hoc多態函數。 因此,Ad-hoc多態性是在其他語言中發現的各種特征的通用術語,例如C / C ++中的函數重載或OOP中基於類的調度多態。

與其他形式的ad-hoc多態性相比,Haskell類型類的一個主要賣點是由於允許類型簽名中的任何位置存在多態性而具有更大的靈活性。 例如,大多數語言不會根據返回類型區分重載函數; 類型類可以。

許多OOP語言中的接口有點類似於Haskell的類型類 - 您指定了一組要以ad-hoc多態方式處理的函數名稱/簽名,然后明確描述各種類型如何與這些函數一起使用。 Haskell的類型類使用類似,但具有更大的靈活性:您可以為類型類函數編寫任意類型的簽名,用於實例選擇的類型變量出現您喜歡的任何地方 ,而不僅僅是作為調用方法的對象類型上。

一些Haskell編譯器 - 包括最流行的GHC - 提供了語言擴展,使類型類更加強大,例如多參數類型類 ,它允許您基於多種類型進行ad-hoc多態函數調度(類似於什么在OOP中稱為“多次調度” )。


為了嘗試給你一些它的味道,這里有一些模糊的Java / C#風格的偽代碼:

interface IApplicative<>
{
    IApplicative<T> Pure<T>(T item);
    IApplicative<U> Map<T, U>(Function<T, U> mapFunc, IApplicative<T> source);
    IApplicative<U> Apply<T, U>(IApplicative<Function<T, U>> apFunc, IApplicative<T> source);
}

interface IReducible<>
{
    U Reduce<T,U>(Function<T, U, U> reduceFunc, U seed, IReducible<T> source);
}

請注意,除其他外,我們還定義了泛型類型的接口並定義了一種方法,其中接口類型僅作為返回類型 Pure 不明顯的是,每次使用接口名稱都應該是相同的類型(即,沒有混合實現接口的不同類型),但我不知道如何表達。

在C ++ / etc中,根據this / self隱式參數的類型調度“虛方法”。 (該方法指向對象隱含指向的函數表)

類型類的工作方式不同,可以執行“接口”可以做的所有事情。 讓我們從一個接口無法做的事情的簡單例子開始:Haskell的Read類型。

ghci> -- this is a Haskell comment, like using "//" in C++
ghci> -- and ghci is an interactive Haskell shell
ghci> 3 + read "5" -- Haskell syntax is different, in C: 3 + read("5")
8
ghci> sum (read "[3, 5]") -- [3, 5] is a list containing 3 and 5
8
ghci> -- let us find out the type of "read"
ghci> :t read
read :: (Read a) => String -> a

read的類型是(Read a) => String -> a ,這意味着對於實現Read類型的每個類型, read都可以將String轉換為該類型。 這是基於返回類型的調度,不可能使用“接口”。

這不能在C ++等人的方法中完成,其中函數表是從對象中檢索的 - 在這里,你甚至沒有相關的對象,直到read它之后才能調用它?

與允許這種情況發生的接口的關鍵實現差異是,函數表未指向對象內部,它由編譯器單獨傳遞給被調用函數。

另外,在C ++ / etc中,當一個人定義一個類時,他們也負責實現他們的接口。 這意味着您不能僅僅發明一個新接口並使Intstd::vector實現它。

在Haskell中你可以,而不是像Ruby中那樣“猴子修補”。 Haskell有一個很好的名稱間距方案,這意味着兩個類型類都可以具有相同名稱的函數,並且類型仍然可以實現兩者。

這允許Haskell有許多簡單的類,如Eq (支持相等性檢查的類型), Show (可以打印到String類型), Read (可以從String解析的類型), Monoid (具有連接操作的類型)和一個空元素)以及更多,並允許甚至像Int這樣的原始類型實現適當的類型類。

隨着類型類的豐富性,人們傾向於編程為更一般的類型,然后具有更多可重用的功能,並且因為當類型一般時它們也具有較少的自由度,它們甚至可以產生更少的錯誤!

tldr:type-classes ==太棒了

除了xtofl和camccann已經在他們的優秀答案中編寫的內容之外,在將Java的接口與Haskell的類型類進行比較時需要注意的有用事項如下:

  1. Java接口是封閉的 ,這意味着任何給定類實現的接口集都是在定義它的時間和地點一勞永逸地決定的;

  2. Haskell的類型類是開放的 ,這意味着任何類型(或多參數類型類的類型組)都可以隨時成為任何類型類的成員,只要可以為類型定義的函數提供合適的定義。類。

類型類的開放性(和Clojure的協議非常相似)是一個非常有用的屬性; Haskell程序員通常會想出一個新的抽象,並通過巧妙地使用類型類立即將它應用於涉及預先存在的類型的一系列問題。

類型類可以與“實現”接口的概念進行比較。 如果Haskell中的某些數據類型實現了“Show”接口,則它可以與期望“Show”對象的所有函數一起使用。

使用OOP,您繼承了接口和實現。 Haskell類型允許將它們分開。 兩個完全不相關的類型都可以暴露相同的接口。

也許更重要的是,Haskell允許在“事后”添加類實現。 也就是說,我可以創建一些我自己的新類型,然后去使所有標准的預定義類型成為這個類的實例。 在OO語言中,無論多么有用,你[通常]都不能輕易地向現有類添加新方法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM