型クラス

このページでは、Haskell における型クラスの概念について説明します。

型クラスとは

型クラス (type class) は、データ型をカテゴライズする役割を持つ概念です。 例えば、数値型全般を表す Num 型クラスというものが存在します。 Num 型クラスの インスタンス (instance) は、具体的な数値型である IntDouble などです。

Numクラス

型クラスは、オブジェクト指向プログラミングにおける「クラス」と似通った概念ですが、レイヤが違う話なので注意が必要です。 オブジェクト指向プログラミングにおけるクラスはデータ型であり、インスタンスはオブジェクトですが、Haskell における型クラスはデータ型のひとつ上の概念であり、インスタンスがデータ型です。

型クラス制約

型クラスの存在意義がわかる簡単な例題として、リストの要素の和を求める関数 sum を考えてみましょう。

sum []     = 0
sum (x:xs) = x + sum xs

この関数 sum の型はどのように書くのが適切でしょうか。 次のように書いてしまうと、Int 以外の数値型のリストに sum を適用できなくなってしまいます。

sum :: [Int] -> Int

かといって、次のような型だと、数値以外のリストにも sum を適用できることになってしまい、不適切です。

sum :: [a] -> a

そこで、型クラスを用いることで、数値のリストにのみ sum を適用できるようにすることができます。

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

この型シグネチャ宣言における => の左辺 Num a は、「型 aNum クラスのインスタンスである」という制約を表します。 この制約は、型クラス制約、あるいは文脈 (context) と呼ばれます。

複数の制約がある場合には、次の例のように書きます。

f :: (C a, D a, E b) => [a] -> [b] -> [a]

型クラスの定義

等値性が評価可能な型全般を表す型クラス Eq が標準で定義されています。 型クラス Eq は、class 宣言によって次のように定義されています。

class Eq a where
  (==), (/=) :: a -> a -> Bool

  x == y = not (x /= y)
  x /= y = not (x == y)

型クラスには、その型クラス特有の関数である、クラスメソッド (class method) を定義できます。 class 宣言には、クラスメソッドの型シグネチャ宣言とデフォルト定義を記述します。 なお、デフォルト定義は省略可能です。

Eq クラスは、(==)(/=) という 2 つのクラスメソッドを持ちます。 デフォルト定義を見ると、(==)(/=) によって、(/=)(==) によって定義されているため、後述 instance 宣言で少なくとも片方を実装する必要があります。

インスタンス宣言

Eq クラスのインスタンスとして、2 次元空間上の点を表す Point 型を考えます。 PointEq クラスのインスタンスであることを示すには、instance 宣言を記述します。

data Point = Pt Double Double

instance Eq Point where
  (Pt x y) == (Pt x' y') = x == x' && y == y'

main = do print $ (Pt 1 2) == (Pt 2 3)  -- 出力: False
          print $ (Pt 1 2) /= (Pt 2 3)  -- 出力: True
          print $ (Pt 1 2) == (Pt 1 2)  -- 出力: True

クラスの継承

順序付けの可能な型全般を表す型クラス Ord が標準で定義されています。

順序付けの可能な型は、等価性の評価も当然できると考えることができます。 これを受けて、Ord クラスは Eq クラスの拡張として、次のように定義されています。

data Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<), (<=), (>), (>=) :: a -> a -> Bool
  max, min :: a -> a -> a

  -- (デフォルト実装は省略)

data Ordering = EQ | LT | GT

Eq a => Ord a は、Ord クラスのインスタンスは Eq クラスのインスタンスでもなければならないという制約を表します。 つまり、Ord クラスのインスタンスは、Eq クラスのクラスメソッド (==), (/=) も実装する必要があります。

型クラス Ord, Eq の関係は、Ord クラスは Eq クラスを継承 (inherit) する、とも表現されます。 このとき、Eq クラスはスーパークラス (superclass)、Ord クラスはサブクラス (subclass) と呼ばれます。

複数のクラスを継承(多重継承)する場合、class 宣言は次の例のように記述します。

class (C a, D a) => E a where ...

次のプログラムは、1 次元空間上の点を表す Point 型を定義したものです。

data Point = Pt Double

instance Eq Point where
  (Pt x) == (Pt x') = x == x'

instance Ord Point where
  compare (Pt x) (Pt x')
    | x == x'   = EQ
    | x < x'    = LT
    | otherwise = GT

main = do print $ (Pt 1) == (Pt 2)  -- 出力: False
          print $ (Pt 1) >= (Pt 2)  -- 出力: False
          print $ (Pt 1) <= (Pt 2)  -- 出力: True

deriving 宣言

クラスメソッドの実装方法が自明な場合、deriving 宣言を用いると、instance 宣言を省略できます。

data Point = Pt Double Double deriving Eq

main = do print $ (Pt 1 2) == (Pt 1 2)  -- 出力: True
          print $ (Pt 1 2) == (Pt 2 3)  -- 出力: False

deriving 宣言に複数の型クラスを指定するには、次のように書きます。

data Point = Pt Double Double deriving (Show, Read, Eq)

標準の型クラス

標準で定義されている型クラスには、次のようなものがあります。

カインド

リストや Maybe 型は、ある値を箱の中に入れたような構造を取ります。 こうした型を表す型クラス Container を、次のように定義します。

class Container c where
  cmap :: (a -> b) -> c a -> c b

関数 cmap は、関数 map の一般化であり、コンテナの中の値を操作します。 次のプログラムは、MaybeContainer クラスのインスタンスとしたものです。

class Container c where
  cmap :: (a -> b) -> c a -> c b

instance Container Maybe where
  -- cmap :: (a -> b) -> Maybe a -> Maybe b
  cmap f Nothing  = Nothing
  cmap f (Just x) = Just (f x)

main = do print $ cmap (2*) Nothing   -- 出力: Nothing
          print $ cmap (2*) (Just 3)  -- 出力: Just 6

関数 cmap の型宣言を吟味すると、Container クラスのインスタンスは Maybe であって、Maybe a ではないことがわかります。 Container クラスのインスタンスは、具体型を取り具体型を返す、一種の高階の型であると言えます。

Container クラスのインスタンスのカインド (kind) は、* -> * と書かれます。 ここで、カインド * は具体的な型を表します。 カインド * -> * は、具体的な型を取り、具体的な型を返す型を表します。 Maybe :: * -> * であり、Int :: * なので、Maybe Int のカインドは * です。