ushumpei’s blog

生活で気になったことを随時調べて書いていきます。

データ型と型クラスの違い

「すごいHaskellたのしく学ぼう!」を勉強した時に、データ型と型クラスで混乱してしまったので、メモ書いておきます。

ザックリまとめると、こんな感じです。

  • データ型:データ構造を定義するもの
  • 型クラス:振る舞いを定義するもの

ちゃんとした内容は「すごいHaskellたのしく学ぼう!」の7章に書いてあります。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

以下簡単な例と概要を書いていきます。

データ型(data)

データ型の例としては、IntWordIntegerFloatDoubleCharBoolOrdering[]()Maybeなどがあります。

データ型の扱いは他のプログラミング言語におけるデータ型と大体同じかと思います。ただし、定義には2種類のコンストラクタと呼ばれるものが出てくるので注意しなければいけません。

値コンストラクタ(Data Constructor)

値コンストラクタはデータ型の値を作成するために使用する関数です。データ型の定義において等号の右側に記述されます。例を見てみます。

data Foo = Bar Int | Baz Float | Qux Int Float

データ型Fooの値コンストラクタはBarBazQuxです。例えばBar値コンストラクタにIntデータ型の値を渡すことでFooデータ型の値を作ることができます。

ghci>let bar = Bar 1
ghci>:t bar
bar :: Foo

つまりBar

Bar :: Int -> Foo

という関数になっています。

違う例でいくと、基本的なデータ型BoolではFalseTrueという値コンストラクタを持ちます。

data Bool = False | True

FalseTrueを定数と考えるか、引数0の値コンストラクタ関数と考えるかは自由ですが、僕は引数0の値コンストラクタと考える方がデータ型の定義が一般化できるので好きです。

型コンストラクタ(Type Constructor)

型コンストラクタはデータ型を変数にとり新たなデータ型を定義します。テータ型の定義では等号の左側に現れます。例をもとに見ていきましょう。

data Maybe a = Nothing | Just a

Maybeデータ型の等号の右側には、二つの値コンストラクタ(引数0、引数1)が並んでいます。ところがJustコンストラクタは引数のデータ型がaとなっていて、こんなデータ型ないよ、と思うわけです。

一方等号の左側にはMaybe aという表記が見えます。このMaybeの部分が型コンストラクタです。この型コンストラクタは色々なデータ型aに対し、いろいろなデータ型Maybe aを作ることができると言っています。

実際に使ってみます。

ghci>let maybe_char = Just 'c'
ghci>:t maybe_char
maybe_char :: Maybe Char

ここではJust値コンストラクタにCharデータ型の'c'を渡すことで、Maybe Charデータ型の値maybe_charを作っています。

Javaで配列の定義でint[]String[]と書いたことや、総称型のワイルドカードの概念にやや似ていますね。

Maybe自体に直接データ型を指定してデータ型を得ることは関数定義の時などですが、型推論に頼ってばかりの僕にはあまり関係ないみたいです。

ちなみにGHCiでは:tに続けて型名を入力すれば型の情報を教えてくれます。定義、インスタンスなどを確認したい場合は:iが便利で結構万能です。

型クラス(type class)

型クラスはインターフェースのようなものです。データ型に実装されたり、違う型クラスに継承されたりします。型クラスの例としては、NumEnumShowReadFunctorApplicativeMonad...などたくさんあります。

型クラスはある性質を一般化したものだと考えられます。例えばNumは整数、浮動小数などの「数」という概念が持つべき振る舞いを定義していますし、Enumは列挙するという振る舞いを定義しています。

データ型には複数の型クラスを実装させることができます。継承も複数可能です。自由です。今回は大変なので継承は扱わず実装のみ見ていくことにします。

型クラスの定義はclassによって宣言します。

class Hoge a where
  fuga :: a -> Int

この型クラスを実装したいデータ型Piyoは、関数fugaを実装しなければいけません。実装はinstanceで宣言します。

instance Hoge Piyo where
  fuga Piyo = 0

(例がとても酷くて申し訳ないです...)

例が例なのに具体的ではなかったので、具体的に初期状態でインポートされているNum型クラスを見てみましょう。

ghci>:i Num
class Num a where
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a

僕がよくデータ型だと間違えてしまうのがこのNumです。これはIntWordFloatDoubleなどの数値系のデータ型がもつ、足す、引く、かけるといった、数としての振る舞いを定義しています。(ちなみにnegateは符号の反転、signumは符号を返しそうです、実際Intではそのような実装が与えられます)

これに対しIntは以下のような実装を与えています。参考

instance  Num Int  where
    I# x + I# y = I# (x +# y)
    I# x - I# y = I# (x -# y)
    negate (I# x) = I# (negateInt# x)
    I# x * I# y = I# (x *# y)
    abs n  = if n `geInt` 0 then n else negate n

    signum n | n `ltInt` 0 = negate 1
             | n `eqInt` 0 = 0
             | otherwise   = 1

    {-# INLINE fromInteger #-}   -- Just to be sure!
    fromInteger i = I# (integerToInt i)

...ちょっとよくわかりません。#はmagic hashと呼ばれるものらしく、後置修飾子として変数に#を付与できるようにするものみたいです(参考)。I#GHC.Types.I#のことで、おそらくこの中で定義した和や差の処理を実装として与えているのだと思います。

感想

良い例が全然出せず残念でした。申し訳ないです。

収穫としてはghcglasgow haskell compilerの略だと知ったことです。

あとこれだけ書いたらもう、データ型と型クラスをごっちゃにすることはなさそうです。