データ型と型クラスの違い
「すごいHaskellたのしく学ぼう!」を勉強した時に、データ型と型クラスで混乱してしまったので、メモ書いておきます。
ザックリまとめると、こんな感じです。
- データ型:データ構造を定義するもの
- 型クラス:振る舞いを定義するもの
ちゃんとした内容は「すごいHaskellたのしく学ぼう!」の7章に書いてあります。
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (73件) を見る
以下簡単な例と概要を書いていきます。
データ型(data)
データ型の例としては、Int
、Word
、Integer
、Float
、Double
、Char
、Bool
、Ordering
や[]
、()
、Maybe
などがあります。
データ型の扱いは他のプログラミング言語におけるデータ型と大体同じかと思います。ただし、定義には2種類のコンストラクタと呼ばれるものが出てくるので注意しなければいけません。
値コンストラクタ(Data Constructor)
値コンストラクタはデータ型の値を作成するために使用する関数です。データ型の定義において等号の右側に記述されます。例を見てみます。
data Foo = Bar Int | Baz Float | Qux Int Float
データ型Foo
の値コンストラクタはBar
、Baz
、Qux
です。例えばBar
値コンストラクタにInt
データ型の値を渡すことでFooデータ型の値を作ることができます。
ghci>let bar = Bar 1 ghci>:t bar bar :: Foo
つまりBar
は
Bar :: Int -> Foo
という関数になっています。
違う例でいくと、基本的なデータ型Bool
ではFalse
、True
という値コンストラクタを持ちます。
data Bool = False | True
False
、True
を定数と考えるか、引数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)
型クラスは、データ型に実装されたり、違う型クラスに継承されたりします。型クラスの例としては、Num
、Enum
、Show
、Read
、Functor
、Applicative
、Monad
...などたくさんあります。
型クラスはある性質を一般化したものだと考えられます。例えば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
です。これはInt
、Word
、Float
、Double
などの数値系のデータ型がもつ、足す、引く、かけるといった、数としての振る舞いを定義しています。(ちなみに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#
のことで、おそらくこの中で定義した和や差の処理を実装として与えているのだと思います。
感想
良い例が全然出せず残念でした。申し訳ないです。
収穫としてはghc
がglasgow haskell compiler
の略だと知ったことです。
あとこれだけ書いたらもう、データ型と型クラスをごっちゃにすることはなさそうです。