Haskell データ型・型クラス・型族

Haskell は用語の使い方が独自だが、考え方は C++やJavaとそんなに変わらない。

Haskell には、型によるエラーチェックを壊すような、ナル値や、ダウンキャスト (基底クラスから派生クラスへの型変換) がない。ダウンキャストがないので, void*のような汎用ポインタも使えない。

Haskell のデータ型 (algebraic datatype; または単に型) が、C++, Javaの (具象) クラスに当たる。Haskell の型クラス (type class; または単にクラス) は、C++の抽象クラスやJavaのインタフェイス。

データ型

データ型は dataキーワードで宣言する。Haskell がややこしいのは、次の3つを同じdataキーワードを使うこと。

  1. 列挙型 (C++, Java enum)
  2. 構造体 (C++ struct / Java class)
  3. 共用体 (C++ union)

また、構造体や共用体の派生で, レコード構文がある。

データ型の宣言の構文はこのようになっている;

topdecl data [context =>] simpletype [= constrs] [deriving]
simpletype 型名 型変数1 ... 型変数k (k ≧ 0)
constrs constr1 | ... | constrn (n ≧ 1)
deriving deriving ( dclass | (dclass1, ... , dclassn) ) (n ≧ 0)

simpletype では, k ≧ 0, つまり型変数が一つもなくても構わない.

constr の部分が宣言する内容。まとめて書くと複雑になるので、列挙型, 構造体, 共用体に分けて説明する。

なお, data宣言における context => の部分は, 規格上は残っているが、実装では廃止され、エラーになる。字面上, データ型の型変数に対する型クラスの制約を指定するように見えるが、そうではなく、コンストラクタの引数に対する制約になる。意味がない。

列挙型

例。

data Color = Blue | Red | Green | White

型名は英大文字で始めること。

この例の Blue などは, コンストラクタ (構築子). これらのコンストラクタが Color型の値を生成する。

構文としては, コンストラクタの名前は、型名と同じでもいいし、違ってもいい。列挙型としての使い方だと, 型名と違う名前にすることが多い。

コンストラクタは英大文字で始まる必要がある。使うときには関数のように式の中で使用するが、普通の関数の名前とは違う。

構造体

構造体の宣言はこのように書く。まずは、フィールド名がない書き方。

data 型名 "=" 構築子 [ フィールドの型 ... ]

例。

data Point = Pt Int Int

この例の Ptが構築子、二つのInt型のフィールドを取る。この用途では, 構築子は型名と同じにすることが多い。型が来る場所と関数が来る場所は構文上明らかなので、同名でも文脈から区別できる。

型の順序だけだと意味が自明ではないので、複雑になる場合は, 後述のレコード構文でフィールドに名前を付ける。

フィールド値を取り出すのは、こんな感じ。関数を作ってやる。

data Point = Pt Int Int

pointX :: Point -> Int   -- 型を書くところでは Point
pointX (Pt x _) = x      -- 式を書くところでは Pt

pointY :: Point -> Int
pointY (Pt _ y) = y

main :: IO ()
main = do
  let pt = Pt 10 20
  print $ pointX pt
  print $ pointY pt

共用体

上の列挙型と構造体を組み合わせたもの。

data データ型 "=" 構築子 [ フィールドの型 ... ] "|" 構築子 [ フィールドの型 ... ] | ...

例。

data Foo = Bar Int String | Baz Int Int Int

この例のBar, Baz はコンストラクタ。C++だとフラグのフィールドを用意して, 開発者がその値によってフィールドの意味を管理しなければならないが、Haskell ではコンストラクタで区別されるので、共用体のなかのフィールドの順序、型について、コンパイラが曖昧さなくエラーチェックできる。

各コンストラクタのフィールドの型、数は同じでなくても構わない。

レコード構文

構造体や共用体の各フィールドには、名前を付けることができます (レコード構文)。C++ / Javaのクラスに近いイメージです。

data データ型 "=" 構築子 "{" フィールド名 :: 型 , ...  "}"

フィールド名は英小文字始まりです。

$ ghci
> data Point = Pt { pointX :: Int, pointY :: Int }
> let mypt = Pt { pointX = 10, pointY = 30 }
> print (pointX mypt)
10

Point型の値を作って, そのフィールド値を表示します。フィールド名の関数が自動的に作られるようなイメージです。あまり名前が被らないように, 型名をフィールド名の先頭に付けるといいでしょう。

多相型

data宣言で, 型名の後ろに型変数を付けることができる。

data 型名 型変数 ... = コンストラクタ [ "|" コンストラクタ ... ]

例えば、任意の型を取れる MyPair 型を作ってみる。

data MyPair a b = MakePair a b

pairFirst :: MyPair a b -> a
pairFirst (MakePair x _) = x

pairSecond :: MyPair a b -> b
pairSecond (MakePair _ y) = y

main :: IO ()
main = do
  let pair = MakePair 10.2 "fuga" :: MyPair Double String
  print $ pairFirst pair
  print $ pairSecond pair

data MyPair に型変数を2つ付けている。

標準ライブラリの Maybe a型は、次のようになっている。

data Maybe a = Nothing | Just a  

NothingJust がコンストラクタ。

型クラス

型クラスで関数のoverload を行う。Java などでのインタフェイスの位置づけ。classキーワードで宣言する。

class Num a where
    (+)    :: a -> a -> a
    negate :: a -> a

型クラスを実装する具象クラス (Haskell用語でデータ型) のことを、型クラスのインスタンスと呼ぶ。

Num aa は型変数。型クラス Numを実装するデータ型a になる。data宣言の型変数と使い方が異なる。

この型クラスはメソッド(+)を持ち, その引数と戻り値は a -> a -> a となる。

Int は型クラスNumを実装する。型がクラスを実装することを宣言するのは, instanceキーワードを使う。

instance Num Int where
    x + y    = ... 定義の実装
    negate x = ... 定義の実装

デフォルト実装

型クラスは、デフォルト実装を持つことができる。Java でもJava 8からインタフェイスにデフォルト実装を持てるようになった。C++でも抽象クラスにメソッド定義ができる。

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

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

デフォルト実装があることで、型クラスEqのインスタンスでは, (==)(/=) のどちらか片方だけ実装すればよい。

型に依存しないアルゴリズムなどは、型クラスのほうで定義することで、汎用的になる。ただ、使えるメソッドが限定されすぎる。すでに見たような、data宣言の多相型でもよい。

型クラスの継承

型クラスは, 別の型クラスから継承することができる。一方、データ型は継承できない。いわゆるオブジェクト指向言語が何でもかんでもクラス (Haskell用語だとデータ型) に機能が押し込まれているのを、分離したイメージ。

型クラス Ord は, Eq を継承している。

$ ghci
> :i Ord
class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  (>) :: a -> a -> Bool
  (>=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a
        -- Defined in ‘GHC.Classes’
instance (Ord a, Ord b) => Ord (Either a b)
  -- Defined in ‘Data.Either’
instance Ord Integer
  -- Defined in ‘integer-gmp-1.0.0.0:GHC.Integer.Type’
instance Ord a => Ord [a] -- Defined in ‘GHC.Classes’
...以下いろいろ続く

class宣言で, Eq a => のように書く。

型クラス制約

newtype, type

データ型 (datatype) は dataキーワード。名前付けデータ型は newtype キーワード, 別名は typeキーワードで宣言する。

型族

●●説明