typeclassesとAbstract Data Typesの違いは何ですか?Scala:typeclassとADTの違いは?
これはHaskellのプログラマのための基本的なことですが、私はScalaのバックグラウンドから来ており、Scalaの例に興味があります。私が今見つけることができる最高のものは、型締めが「開いている」こととADTが「閉じている」ということです。また、タイプクォーターと構造タイプを比較して対比することも役立ちます。
typeclassesとAbstract Data Typesの違いは何ですか?Scala:typeclassとADTの違いは?
これはHaskellのプログラマのための基本的なことですが、私はScalaのバックグラウンドから来ており、Scalaの例に興味があります。私が今見つけることができる最高のものは、型締めが「開いている」こととADTが「閉じている」ということです。また、タイプクォーターと構造タイプを比較して対比することも役立ちます。
の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部](http://chris-taylor.github.io/blog/2013/02/10/the-algebra-of-algebraic-data-types/)は良い場所ですこの番号/データ型の対応に興味がある場合は、開始してください。 – kqr
すばらしい答え、ScalaとHaskellで例を書く時間をとってくれてありがとう。 –
型クラスと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
がとる枝を知ることができませんe
。 e
がLeft
の場合は、Left
ブランチを使用し、e
がRight
の場合は、Right
ブランチを使用します。時々私は、静的コンストラクタe
がされるについて推論することができますが、時々私は、次の例のように、することはできません。このような状況でも、別の概念ではありません抽象データ型、ある
main = do
e <- readLn -- Did I get a 'Left' or 'Right'?
print2 e -- Who knows until I run the program
あなたの質問は、実際に明確なコンセプトに触れます。 「抽象」データ型と「代数型」データ型の両方を「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
:より現実的な例では、このような何かを探して、従業員のレコードであるかもしれません。例えば、我々はBar
とBaz
の両方を持つことができます。
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)をご覧ください。
今、何について約抽象データ型ですか?これは別の概念を指します。抽象データ型は、実装が公開されていないものです。実際に型の値がどのように見えるかはわかりません。あなたがそれで行うことができる唯一のことは、モジュールからエクスポートされた適用関数です。それにパターンを当てたり、新しい値を自分で作ることはできません。実際の良い例はMap
(Data.Map
から)です。マップは実際には特定の種類のバイナリ検索ツリーですが、モジュール内の何もツリー構造で直接作業することはできません。これは重要なことです。なぜなら、あなたは簡単に邪魔になるかもしれないある種の追加の不変量をツリーが維持する必要があるからです。したがって、不透明ブロブとしてMap
を使用するだけです。
代数型と抽象的な型は、幾分直交した概念です。彼らの名前が間違って他の人を間違えさせるようになるのはむしろ残念です。
パズルの最後の部分は、タイプです。代数的および抽象的なデータ型とは異なり、型クラスは型自体ではありません。むしろ、種類のセットとタイプクラフトを考えてください。特に、typecassは、特定の機能を実装するすべての型のセットです。
最も単純な例は、文字列表現を持つすべてのタイプのクラスであるShow
です。つまり、すべてのタイプa
には、show ∷ a → String
の機能があります。タイプがshow
の機能を持つ場合、それは "in Show
"です。そうでなければ、そうではありません。 Int
、Bool
、String
のような大部分のタイプはすべてShow
です。一方、関数(→
を持つ任意のタイプ)は、ではなく、であり、Show
である。このため、GHCiは関数を出力できません。
typeclassは、型が実装する必要がある関数によって定義されます。例えば、Show
はshow
機能によってちょうどdefined²ことができます:
class Show a where
show ∷ a → String
は今Show
にFoo
のような新しいタイプを追加するために、我々はそれをインスタンスを記述する必要があります。この後
instance Show Foo where
show foo = case foo of
Bar → "Bar"
Baz → "Baz"
、Foo
がShow
である:これはshow
機能の実際の実装です。どこでもFoo
のインスタンスを書くことができます。特に、たとえ他のモジュールであっても、クラスが定義された後に新しいインスタンスを書くことができます。これは、タイプメスがになることを意味します。;代数的データ型とは異なり、私たちは事実の後に新しいものを型式に追加することができます。
typeclassesもあります。 same LYAH chapterでそれらについて読むことができます。
¹技術的には、⊥(bottom)と呼ばれる別の値もありますが、ここでは無視します。後で⊥について学ぶことができます。実際には
²、Show
は実際にString
にリストa
の秒かかり、別の可能な機能を持っています。これは基本的に、文字列がそれ自身の型ではなくChar
のリストにすぎないので、文字列をきれいに見せるためのハックです。
*代数*データ型を意味しますか? – Carl
申し訳ありませんが、OO世界では、ADTは抽象データ型を意味する傾向がありますが、FP世界では代数データ型を意味するため、ちょっと混乱します。これをクリアする皆様に感謝します。 –