型クラスと抽象データ型の違いは何ですか?
これは Haskell プログラマーにとって基本的なことですが、私は Scala のバックグラウンドを持っているので、Scala の例に興味があります。私が今見つけることができる最高のものは、型クラスが「オープン」であり、ADT が「クローズ」であるということです。型クラスと構造型を比較対照することも役に立ちます。
型クラスと抽象データ型の違いは何ですか?
これは Haskell プログラマーにとって基本的なことですが、私は Scala のバックグラウンドを持っているので、Scala の例に興味があります。私が今見つけることができる最高のものは、型クラスが「オープン」であり、ADT が「クローズ」であるということです。型クラスと構造型を比較対照することも役に立ちます。
ADT (このコンテキストでは、別の概念である抽象データ型ではなく、代数データ型) と型クラスは、異なる問題を解決する完全に異なる概念です。
ADT は、頭字語から次のように、データ型です。データを構造化するには ADT が必要です。Scala に最も近いのは、ケース クラスと封印されたトレイトの組み合わせだと思います。これは、Haskell で複雑なデータ構造を構築する主な手段です。Maybe
ADT の最も有名な例は型だと思います。
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)
; より高いアリティも可能です)。
次のデータ型を検討してください。
-- 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
。これが意味することは、タイプの変数がある場合、それは aまたは a の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"
}
インターフェイスと型クラスの間には非常に重要な違いがあります。まず、カスタム型クラスを作成し、任意の型をそのインスタンスにすることができます。
class MyShow a where
myShow :: a -> String
instance MyShow Int where
myShow x = ...
ただし、インターフェイスではそのようなことはできません。つまり、既存のクラスにインターフェイスを実装させることはできません。お気づきのように、この機能は型クラスがopenであることを意味します。
既存の型に型クラス インスタンスを追加するこの機能は、式の問題を解決する方法です。Java 言語にはそれを解決する手段がありませんが、Haskell、Scala、または Clojure にはあります。
型クラスとインターフェイスのもう 1 つの違いは、インターフェイスが最初の引数、つまり Implicit でのみポリモーフィックであること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 には、インターフェースと型クラスを組み合わせたプロトコルがあります。プロトコルは単一の最初の引数でディスパッチされますが、既存の任意の型のプロトコルを実装できます。