型クラス
このページでは,Haskell における型クラスの概念について説明します.
1. 型クラスとは
型クラス(type class)は,データ型をカテゴライズする役割を持つ概念です.
例えば,数値型全般を表す Num
という型クラスは,数値型全般を表します.
Num
型クラスの インスタンス(instance)は,具体的な数値型である Int
や Double
などです.
型クラスは,オブジェクト指向プログラミングにおける "クラス" と似通った概念ですが,レイヤが違う話なので注意が必要です. オブジェクト指向プログラミングにおけるクラスはデータ型であり,インスタンスはオブジェクトですが,Haskell における型クラスはデータ型のひとつ上の概念であり,インスタンスがデータ型です. |
2. 型クラス制約
型クラスの存在意義がわかる簡単な例題として,リストの要素の和を求める関数 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
は,"型 a
は Num
クラスのインスタンスである" という制約を表します.
この制約は,型クラス制約,あるいは文脈(context)と呼ばれます.
複数の制約がある場合には,次の例のように書きます.
f :: (C a, D a, E b) => [a] -> [b] -> [a]
3. 型クラスの定義
等値性が評価可能な型全般を表す型クラス 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
宣言で少なくとも片方を実装する必要があります.
4. インスタンス宣言
Eq
クラスのインスタンスとして,2 次元空間上の点を表す Point
型を考えます.
Point
が Eq
クラスのインスタンスであることを示すには,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
5. クラスの継承
順序付けの可能な型全般を表す型クラス 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
6. 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)
7. 標準の型クラス
標準で定義されている型クラスには,次のようなものがあります.
-
Eq クラス: 等値性が評価可能な型を表します.
-
Ord クラス: 順序付けの可能な型を表します.
-
Num クラス: 数値として扱われる型を表します.インスタンスには Int, Integer, Float, Double 等があります.
class Num a where
(+), (*), (-) :: a -> a -> a
negate, abs, signum :: a -> a
fromInteger :: Integer -> a
-
Show クラス: 文字列化可能な型を表します.
data Point = Pt Double Double deriving Show
main = do putStrLn $ show (Pt 2 3) -- 出力: Pt 2.0 3.0
print (Pt 2 3) -- 出力: Pt 2.0 3.0
-
Read クラス: 文字列から変換可能な型を表します.
data Point = Pt Double Double deriving (Read, Show)
main = do print (read "Pt 3 4" :: Point) -- 出力: Pt 3.0 4.0
-
Enum クラス: 列挙型を表します.列挙型を要素とするリストには,数列表記が使えます.
data Day = Sun | Mon | Tue | Wed | Thu | Fri | Sat
deriving (Enum, Show)
main = print [Mon .. Fri] -- 出力: [Mon, Tue, Wed, Thu, Fri]
-
Bounded クラス: 上限,下限を持つ型を表します.
data Day = Sun | Mon | Tue | Wed | Thu | Fri | Sat
deriving (Enum, Bounded, Show)
main = do print $ (minBound :: Day) -- 出力: Sun
print $ (maxBound :: Day) -- 出力: Sat
8. カインド
ここでは,"型の型" に相当する概念である カインド(kind)を紹介します.
リストや Maybe
型は,ある値を箱の中に入れたような構造を取ります.
こうした型を表す型クラス Container
を,次のように定義してみます.
class Container c where
cmap :: (a -> b) -> c a -> c b
関数 cmap
は,関数 map
の一般化であり,コンテナの中の値を操作します.
次のプログラムは,Maybe
を Container
クラスのインスタンスとしたものです.
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
クラスのインスタンスのカインドは,* -> *
と書かれます.
ここで,カインド *
は何らかの具体的な型を表します.
カインド * -> *
は,具体的な型を取り,具体的な型を返す型を表します.
Maybe :: * -> *
であり,Int :: *
なので,Maybe Int
のカインドは *
です.