11

類型類和抽象數據類型有什麼區別?Scala:類型類和ADT之間的區別?

我知道這是Haskell程序員的基本功能,但我來自Scala背景,並且對Scala中的示例感興趣。我現在能找到的最好的結果是,類型類是「開放的」,ADT是「關閉的」。將類型類與結構類型進行比較和對比也是有幫助的。

+2

你的意思是*代數*數據類型? – Carl

+0

對不起,我猜在OO世界裏,ADT往往意味着抽象數據類型,但在FP世界中它意味着代數數據類型,所以我有點困惑。感謝大家清理這個。 –

回答

46

的ADT(在這種情況下是不是抽象的數據類型,甚至是另外一個概念,但代數數據類型)和類型類是解決不同問題的完全不同的概念。

從首字母縮寫詞開始,ADT是一種數據類型。需要ADT才能構建您的數據。我認爲斯卡拉中最接近的匹配是案例類和密封特徵的組合。這是在Haskell中構建複雜數據結構的主要手段。我認爲ADT的最著名的例子是Maybe類型:

data Maybe a = Nothing | Just a 

該產品具有標準Scala庫直接等同,叫Option

sealed trait Option[+T] 
case class Some[T](value: T) extends Option[T] 
case object None extends Option[Nothing] 

這不是Option究竟是如何在定義標準庫,但你明白了。

基本上ADT是幾個命名元組的組合(在某種意義上)(0進制,作爲Nothing/None; 1進制,作爲Just a/Some(value);更高arities也是可能的)。

考慮以下數據類型:

-- Haskell 
data Tree a = Leaf | Branch a (Tree a) (Tree a) 
// Scala 
sealed trait Tree[+T] 
case object Leaf extends Tree[Nothing] 
case class Branch[T](value: T, left: Tree[T], right: Tree[T]) extends Tree[T] 

這是簡單的二進制樹。這兩個定義基本上如下所示:「二叉樹是LeafBranch;如果它是分支,則它包含一些值和另外兩棵樹」。這意味着如果您有一個類型爲Tree的變量,那麼它可以包含一個LeafBranch,您可以檢查哪一個在那裏,並在需要時提取包含的數據。對於這樣的檢查和提取主平均值是模式匹配:

-- Haskell 
showTree :: (Show a) => Tree a -> String 
showTree tree = case tree of 
    Leaf     -> "a leaf" 
    Branch value left right -> "a branch with value " ++ show value ++ 
          ", left subtree (" ++ showTree left ++ ")" ++ 
          ", right subtree (" ++ showTree right ++ ")" 
// Scala 
def showTree[T](tree: Tree[T]) = tree match { 
    case Leaf => "a leaf" 
    case Branch(value, left, right) => s"a branch with value $value, " + 
            s"left subtree (${showTree(left)}), " + 
            s"right subtree (${showTree(right)})" 
} 

這個概念是很簡單的,但也很厲害。

正如您所注意到的,ADT是已關閉,即在定義類型後不能添加更多命名的元組。在Haskell中,這是在語法上強制執行的,而在Scala中,這是通過sealed關鍵字實現的,該關鍵字不允許其他文件中的子類。

這些類型被稱爲代數的原因。命名元組可以被看作是產品(在數學意義上)和這些元組的「組合」作爲總和(在數學意義上也是如此),這樣的考慮具有深刻的理論意義。例如,前面提到的二叉樹型可以這樣寫:

Tree a = 1 + a * (Tree a) * (Tree a) 

但我認爲這是超出範圍了這個問題。如果您想了解更多信息,我可以搜索一些鏈接。


另一方面,類型類是一種定義多態行爲的方法。粗略的類型類別是特定類型提供的合約。例如,您知道您的價值x滿足定義某些操作的合同。然後,您可以調用該方法,然後自動選擇該合同的實際執行。

一般類型的類與Java接口比較,例如:

-- Haskell 
class Show a where 
    show :: a -> String 
// Java 
public interface Show { 
    String show(); 
} 
// Scala 
trait Show { 
    def show: String 
} 

使用該比較,類型類的實例相匹配的實現的接口:

-- Haskell 
data AB = A | B 

instance Show AB where 
    show A = "A" 
    show B = "B" 
// Scala 
sealed trait AB extends Show 
case object A extends AB { 
    val show = "A" 
} 
case object B extends AB { 
    val show = "B" 
} 

有很imp接口和類型類之間的不同之處。首先,你可以編寫自定義類型的類,並任何類型的一個實例:

class MyShow a where 
    myShow :: a -> String 

instance MyShow Int where 
    myShow x = ... 

但你不能做這種事與接口,也就是說,你不能讓現有類實現你的接口。正如你也注意到的那樣,這個功能意味着類型類別是開放

這種爲現有類型添加類型實例的功能是一種解決expression problem的方法。 Java語言沒有辦法解決它,但Haskell,Scala或Clojure都有。

類型類和接口之間的另一個區別是,接口只在第一個參數上是多態的,即在隱式this上。在這個意義上,類型類不受限制。您可以定義調度即使在返回值類型類:

class Read a where 
    read :: String -> a 

這是不可能的接口來做到這一點。

類型類可以使用隱式參數在Scala中模擬。這種模式非常有用,以至於在最近的Scala版本中甚至有一個特殊的語法來簡化它的使用。下面是它是如何做:

trait Showable[T] { 
    def show(value: T): String 
} 

object ImplicitsDecimal { 
    implicit object IntShowable extends Showable[Int] { 
    def show(value: Int) = Integer.toString(value) 
    } 
} 

object ImplicitsHexadecimal { 
    implicit object IntShowable extends Showable[Int] { 
    def show(value: Int) = Integer.toString(value, 16) 
    } 
} 

def showValue[T: Showable](value: T) = implicitly[Showable[T]].show(value) 
// Or, equivalently: 
// def showValue[T](value: T)(implicit showable: Showable[T]) = showable.show(value) 

// Usage 
{ 
    import ImplicitsDecimal._ 
    println(showValue(10)) // Prints "10" 
} 
{ 
    import ImplicitsHexadecimal._ 
    println(showValue(10)) // Prints "a" 
} 

Showable[T]特徵對應類型的類,並且隱含對象定義對應它的實例。如你所見,輸入類是一種界面,但功能更強大。您甚至可以選擇不同的類型類實現,而使用它們的代碼保持不變。然而,這種能力是以樣板和額外的實體爲代價的。

請注意,可以編寫與上面的Scala程序相同的Haskell,但它需要編寫多個模塊或包裝器,所以我不在這裏介紹它。

BTW,Clojure,一個在JVM上工作的Lisp方言,有協議,它們結合了接口和類型類。協議分派在單個第一個參數上,但是您可以爲任何現有類型實現協議。

+1

[代數數據類型的代數,第1部分](http://chris-taylor.github.io/blog/2013/02/10/the-algebra-of-algebraic-data-types/)是一個好地方如果你對這個數字/數據類型的對應感興趣,就開始吧。 – kqr

+1

很好的答案,感謝您花時間在Scala和Haskell編寫示例。 –

6

類型類和ADT之間的區別是:當你想

  • 使用類型類基於關閉的東西的
  • 使用的ADT派遣一個方法時要調度的方法基於斷事的

。例如,考慮print功能:

print :: (Show a) => a -> IO() 

類型是靜態的,不能在程序的整個生命週期中更改,因此當您使用類型類時,您使用的方法在編譯時根據調用站點處的推斷類型進行靜態選擇。因此,在這個例子中,我知道,我現在用的是Char實例Show甚至沒有運行程序:

main = print 'C' 

的ADT讓你動態修改函數的行爲。例如,我可以定義:

print2 :: Either Char String -> IO() 
print2 (Left c ) = putStrLn [c] 
print2 (Right str) = putStrLn str 

現在,如果我在某些情況下調用print2

print2 e 

...我不知道print2需要哪個分支,除非我知道運行時值e。如果eLeft那麼我拿Left分支,如果eRight然後我拿Right分支。有時候,我可以靜態地推理哪個構造e會,但有時我做不到,比如下面的例子:

main = do 
    e <- readLn -- Did I get a 'Left' or 'Right'? 
    print2 e  -- Who knows until I run the program 
7

你的問題其實倒是不同的概念: 類型類,抽象數據類型和代數數據類型。令人困惑的是,「抽象」和「代數」數據類型可以是簡寫爲「ADT」的 ;在Haskell環境中,ADT幾乎總是意味着 「代數」。

那麼讓我們定義所有三個術語。

代數數據類型(ADT),可以通過組合 更簡單的類型來完成。這裏的核心思想是一個「構造函數」,它是一個定義值的 符號。把它想象成一個 Java風格的枚舉值,除了它也可以帶參數。最簡單的代數 數據類型只有一個不帶參數的構造函數:

data Foo = Bar 

只有one¹這種類型的值:Bar。就其本身而言,這不是很有趣;我們需要一些方法來建立更大的類型。

第一種方法是給我們的構造函數參數。例如,我們可以有我們的Bar。就拿一個int和一個字符串:

data Foo = Bar Int String 

現在Foo有許多不同的可能值:Bar 0 "baz"Bar 100 "abc"等。一個更現實的例子可能是員工的記錄,看起來像這樣:

data Employee = Employee String String Int 

另一種方式來建立更復雜的類型是有多個構造函數可供選擇。例如,我們可以同時擁有Bar一個Baz

data Foo = Bar 
     | Baz 

現在Foo類型的值可以是BarBaz。這實際上是正好布爾如何工作; Bool定義如下:

data Bool = True 
      | False 

它的工作原理與您所期望的完全相同。真正有趣的類型可以使用這兩種方法來自我組合。作爲虛構的例子,假設形狀:

data Shape = Rectangle Point Point 
      | Circle Point Int 

形狀可以是矩形,由它的兩個角部限定,或圓形,其是中心和半徑。 (我們只是將Point定義爲(Int, Int)。)不夠公平。但在這裏,我們遇到了一個障礙:事實證明,其他形狀也存在!如果一些相信三角形的異端人士想要在他們的模型中使用我們的類型,他們可以在事實之後添加一個Triangle構造函數嗎?不幸的是:在Haskell中,代數數據類型是已關閉,這意味着您不能在事實之後添加新的替代方法。

你可以用代數數據類型做的一件重要事情是模式匹配就可以了。這基本上意味着能夠分支ADT的替代品。作爲一個非常簡單的例子,而不是在Bool使用if表達式,你可以模式匹配:

case myBool of 
    True → ... -- true case 
    False → ... -- false case 

如果你的構造函數有參數,也可以通過模式匹配訪問這些值。使用上面Shape,我們可以寫一個簡單的area功能:

area shape = case shape of 
    Rectange (x₁, y₁) (x₂, y₂) → (x₂ - x₁) * (y₂ - y₁) 
    Circle _ r     → π * r^2 

_只是意味着我們不關心點的中心值。

這只是代數數據類型的一個基本概述:事實證明有更多的樂趣。你可能想看看relevant chapter瞭解更多哈斯克爾(簡稱LYAH)的更多閱讀資料。

現在,摘要數據類型怎麼樣?這是指不同的概念。抽象數據類型是實現未公開的類型:您不知道類型的值實際是什麼樣子。你可以用它做的唯一事情就是應用從模塊中導出的函數。您無法在其上匹配模式或自行構建新值。實踐中的一個好例子是Map(來自Data.Map)。地圖實際上是一種特殊的二叉搜索樹,但模塊中沒有任何東西可以讓您直接使用樹結構。這很重要,因爲樹需要保留一些額外的不變量,你可以很容易搞砸。所以你只使用Map作爲一個不透明的斑點。代數和抽象類型有些正交的概念;很不幸的是,他們的名字很容易讓人誤以爲是另一個。

拼圖的最後一塊是型號。類型類不像代數和抽象數據類型,本身不是類型。相反,將類型劃分爲類型的集合。特別是,類型類是實現某些功能的所有類型的集合。

最簡單的例子是Show,它是具有字符串表示的所有類型的類;即所有類型爲a,其中我們有一個功能show ∷ a → String。如果一個類型具有show函數,我們說它是「在Show」;否則,它不是。你知道的大多數類型,如Int,BoolString都在Show;另一方面,功能(具有的任何類型)是而不是Show。這就是GHCi無法打印功能的原因。

類型類定義了類型需要實現哪些功能以成爲它的一部分。例如,Show可以defined²只是由show功能:

class Show a where 
    show ∷ a → String 

我們像Foo添加一個新的類型Show,我們必須寫一個實例它。這是實際執行的show功能:

instance Show Foo where 
    show foo = case foo of 
    Bar → "Bar" 
    Baz → "Baz" 

在此之後,FooShow。我們可以在任何地方編寫Foo的實例。特別是,我們可以在定義類之後編寫新的實例,即使在其他模塊中也是如此。這對於typeclasses來說意味着開放;不像代數數據類型,我們可以在事後添加新的東西給類型類。

還有更多的typeclasses;你可以在same LYAH chapter中閱讀。

¹從技術上講,還有一個叫⊥(bottom)的值,但現在我們將忽略它。您可以稍後瞭解⊥。

²在現實中,Show其實有另一種可能的功能,需要一個列表a s到一個String。這基本上是使字符串看起來很漂亮的一個竅門,因爲字符串只是一個Char的列表,而不是它自己的類型。