11

typeclassesとAbstract Data Typesの違いは何ですか?Scala:typeclassとADTの違いは?

これはHaskellのプログラマのための基本的なことですが、私はScalaのバックグラウンドから来ており、Scalaの例に興味があります。私が今見つけることができる最高のものは、型締めが「開いている」こととADTが「閉じている」ということです。また、タイプクォーターと構造タイプを比較して対比することも役立ちます。

+2

*代数*データ型を意味しますか? – Carl

+0

申し訳ありませんが、OO世界では、ADTは抽象データ型を意味する傾向がありますが、FP世界では代数データ型を意味するため、ちょっと混乱します。これをクリアする皆様に感謝します。 –

答えて

46

のADTを(が、代数データ型)と型クラスは、異なる問題を解決する全く異なる概念です。

ADTは、頭字語から次のように、データ型です。データを構造化するにはADTが必要です。 Scalaの最も近いマッチは、ケースクラスと密封された特性の組み合わせです。これはハスケルの複雑なデータ構造を構築する主な手段です。 Optionと呼ばれる、

data Maybe a = Nothing | Just a 

このタイプは、標準のScalaライブラリーに直接対応しています:私は、ADTの最も有名な例はMaybeタイプだと思う

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

これはOptionがで定義されている正確にどのようにではありません標準的なライブラリが、あなたはポイントを得る。

基本的ADTは、いくつかの名前付きタプル(ある意味で)の組み合わせである(Nothing/Noneとして0進、1進、Just a/Some(value)として、より高いアリティも可能です)。

は、次のデータ型を考えてみましょう:

-- 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] 

これは単純なバイナリツリーです。これらの定義は、基本的に次のように解釈されます。「バイナリツリーはLeafまたはBranchのいずれかで、分岐の場合は値と2つのツリーが含まれます。これは、タイプがTreeの変数を持つ場合は、LeafまたはBranchのいずれかを含むことができ、必要に応じて、そこにあるものをチェックしてデータを抽出することができます。

-- 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を書くことは可能ですが、複数のモジュールまたはnewtypeラッパーを記述する必要があるので、ここでは提示しません。

JVMで動作するLispの方言であるClojureは、のプロトコルを持っています。これは、インターフェイスとタイプクラスを組み合わせています。プロトコルは単一の最初の引数でディスパッチされますが、既存のタイプのプロトコルを実装できます。 型クラス、抽象データ型と代数データ型:

+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() 

タイプは静的であり、プログラムの存続期間中は変更できません。したがって、タイプクラスを使用する場合、使用するメソッドはコールサイトの推論タイプに基づいてコンパイル時に静的に選択されます。したがって、この例では、私は、私もプログラムを実行せずShowためCharインスタンスを使用していますことを知っている:

main = print 'C' 

のADTを使用すると、動的機能の動作を変更しましょう。例えば、私が定義することができます。

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

を今、私はいくつかのコンテキストでprint2を呼び出す場合:

print2 e 

...私は実行時の値を知っている限りprint2がとる枝を知ることができませんeeLeftの場合は、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」と略して とすることもできます。ハスケルの文脈では、ADTはほとんど常に "代数"を意味します。

3つの用語をすべて定義しましょう。

代数データ型(ADT)は、より簡単な型の を組み合わせて作成できる型です。ここでのコアアイデアは、値を定義する のシンボルである「コンストラクタ」です。これは引数を取ることができる点を除いて、 Javaスタイルの列挙型の値と似ています。 Bar

data Foo = Bar 

のみ、このタイプの値one¹があります:最も簡単な 代数データ型は、引数なしでただ1つのコンストラクタを持っています。それだけでは、これはあまり面白くありません。より大きなタイプを構築するには何らかの方法が必要です。

最初の方法は、コンストラクタの引数を与えることです。たとえば、私たちは私たちのBar sがint型と文字列を取ることができます:Bar 0 "baz"Bar 100 "abc"など:

data Foo = Bar Int String 

は今 Fooは、多くの異なる可能な値を持っています。より複雑なタイプを構築する他の方法から選択する複数のコンストラクタを持つことである

data Employee = Employee String String Int 

:より現実的な例では、このような何かを探して、従業員のレコードであるかもしれません。例えば、我々はBarBazの両方を持つことができます。

data Foo = Bar 
     | Baz 

今タイプFooの値はBarまたはBazのいずれかになります。これは実際にはであり、正確にはブーリアンの動作方法です。 Boolは、次のように定義されています。

data Bool = True 
      | False 

期待通りに動作します。本当に面白いタイプは、両方の方法を組み合わせて自分自身を組み合わせることができます。やや不自然な例として、形状を想像:

data Shape = Rectangle Point Point 
      | Circle Point Int 

形状は、いずれかのその2つの角によって規定される矩形、又は中心及び半径の円であってもよいです。 (Point(Int, Int)と定義するだけです)十分に公正です。しかしここでは、私たちは悩みに遭遇します:形も存在することが判明しました!三角形を信じる異端者が自分のモデルで自分の型を使用したいと思っている人は、実際にはTriangleのコンストラクタを追加できますか?残念なことに:Haskellでは、代数的データ型はが閉じたであり、事実の後で新しい代用を追加することはできません。

代数データ型でできることの1つは、パターン一致です。これは基本的に、ADTの代替案を分岐できることを意味します。非常に簡単な例として、代わりにif式を使用して、あなたはBool上のパターンマッチができます:あなたのコンストラクタは引数を持っている場合は、パターンマッチングにより、これらの値を

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

アクセスすることができます。

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

_はちょうど私達がポイントの中央の値を気にしない意味:上からShapeを使用して、我々は簡単なarea関数を書くことができます。

これは、代数的データ型の基本的な概要です。これはかなり楽しくなることが判明しました。 relevant chapterでご覧になることをお勧めします。詳しくは、Haskell(LYAH)をご覧ください。

今、何について約抽象データ型ですか?これは別の概念を指します。抽象データ型は、実装が公開されていないものです。実際に型の値がどのように見えるかはわかりません。あなたがそれで行うことができる唯一のことは、モジュールからエクスポートされた適用関数です。それにパターンを当てたり、新しい値を自分で作ることはできません。実際の良い例はMapData.Mapから)です。マップは実際には特定の種類のバイナリ検索ツリーですが、モジュール内の何もツリー構造で直接作業することはできません。これは重要なことです。なぜなら、あなたは簡単に邪魔になるかもしれないある種の追加の不変量をツリーが維持する必要があるからです。したがって、不透明ブロブとしてMapを使用するだけです。

代数型と抽象的な型は、幾分直交した概念です。彼らの名前が間違って他の人を間違えさせるようになるのはむしろ残念です。

パズルの最後の部分は、タイプです。代数的および抽象的なデータ型とは異なり、型クラスは型自体ではありません。むしろ、種類のセットとタイプクラフトを考えてください。特に、typecassは、特定の機能を実装するすべての型のセットです。

最も単純な例は、文字列表現を持つすべてのタイプのクラスであるShowです。つまり、すべてのタイプaには、show ∷ a → Stringの機能があります。タイプがshowの機能を持つ場合、それは "in Show"です。そうでなければ、そうではありません。 IntBoolStringのような大部分のタイプはすべてShowです。一方、関数(を持つ任意のタイプ)は、ではなく、であり、Showである。このため、GHCiは関数を出力できません。

typeclassは、型が実装する必要がある関数によって定義されます。例えば、Showshow機能によってちょうどdefined²ことができます:

class Show a where 
    show ∷ a → String 

は今ShowFooのような新しいタイプを追加するために、我々はそれをインスタンスを記述する必要があります。この後

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

FooShowである:これはshow機能の実際の実装です。どこでもFooのインスタンスを書くことができます。特に、たとえ他のモジュールであっても、クラスが定義された後に新しいインスタンスを書くことができます。これは、タイプメスがになることを意味します。;代数的データ型とは異なり、私たちは事実の後に新しいものを型式に追加することができます。

typeclassesもあります。 same LYAH chapterでそれらについて読むことができます。

¹技術的には、⊥(bottom)と呼ばれる別の値もありますが、ここでは無視します。後で⊥について学ぶことができます。実際には

²、Showは実際にStringリストaの秒かかり、別の可能な機能を持っています。これは基本的に、文字列がそれ自身の型ではなくCharのリストにすぎないので、文字列をきれいに見せるためのハックです。

関連する問題