ブログを消そうかと思うので、一部の記事をQiitaへ移行してきました。
2018/12/21に投稿したものです。
この記事はHaskell Advent Calendar 2018の21日目の記事です。
こんにちはHaskellビギナーの@mrsekutです。
最近Haskellを始めたのですが、Functorを知るにしても、Monadを知るにしても、型クラスがどういうものかを知る必要があるようなので調べてまとめてみました。
型クラス(Type class)とは
型クラスは、一言で言うと「型の振る舞いを定義するもの」のことです。
ある型Hogeがどんな性質を持つのか?を定義できます。
オブジェクト指向の「クラス」と同様にメソッドを持っていたり、インスタンスを作れたりしますが、レイヤーが異なるので最初は別のものと考えた方がわかりやすいかもしれません。
というのは、オブジェクト指向のクラスは型で、インスタンスはオブジェクトになりますが、型クラスは型より上の概念になり、インスタンスが型になります。
以下の表のような感じです。
オブジェクト指向のクラス | 型クラス | |
---|---|---|
クラス | 型 | 型より上の概念 |
インスタンス | オブジェクト | 型 |
型クラスでのインスタンスとは、その型クラスの制約を満たすようにした「型」のことになります。
また、型クラスのメソッドは、そのインスタンスとなる型の振る舞いを定義したものになります。
具体例を見る
といっても抽象的すぎてよくわからないので、具体例を見てみます。
型クラスShow
Haskellの標準の型クラスの一つにShowがあります。
これは「String型での表現に変換できる」という性質を持つ型クラスで、インスタンスに以下のような型を持ちます。
Int
Bool
String
[Int]
- etc.
これらは以下のようにString型に変換することができます。
-
Int
: 123 → “123” -
Bool
: True → “True” -
String
: “test” → “\”test\”” -
[Int]
: [1,2] → “[1,2]” - etc.
Show型クラスのインスタンスを引数に取る関数
また、showという関数があり、これはShow型クラスのインスタンスにできる型を引数に取ります。
showを使うと実際に値を文字列に変換して出力することができます。
例えばInt型の値に対してshow関数を適用してみるとご覧の通り。
ghci> show 42
"42"
また、show関数の型定義は以下のようになっています。
ghci> :t show
show :: Show a => a -> String
「Show a => a
」というのは「型クラス制約」を示しています。
通常はa
は任意の型を表しますが、今回の場合は、この型a
は「Show型クラスのインスタンスである」必要があります。
つまり、上の定義は「Show型クラスに属する任意の型の引数を一つ取り、String型を返す関数」と読むことができます。
型クラスの種類
型クラスにはShowの他にもたくさんありますし、自分で定義することもできます。
標準で定義されている型クラスの中から、わかりやすいものを一部列挙します。
- Eq
- 等値性が評価可能な型
- Ord
- 順序付け可能な型
- Show
- 文字列型での表現に変換できる
- Num
- 数値として扱われる型
- Fractional
- Num に加え、(整数のものでない)割り算が定義されている
- Floating
- Fractional に加え、特定の計算が定義されている
- 特定の計算とは、三角関数や根号、ネイピア数、対数など
- Integral
- Num に加えて、整数の割り算や剰余などが定義されている
- Enum
- ある値の前後の値が定義されている
中には、「ある型クラスAのインスタンスになるためには、型クラスBのインスタンスでないといけない」、といったものもあります。
例えば、わかりやすいのは、FractionalやIntegralでしょうか。
これらのインスタンスになるためには、Numのインスタンスでないといけません。
こういったものは型クラスを定義する際に、親クラスを継承する形で定義することができます。
型クラスがあると何が嬉しいのか
やっと、なんとなく型クラスのイメージが見えてきました。
で、型クラスがあると何が嬉しいのでしょうか。
型クラスがない世界から考えてみます。
型クラスの存在しない世界で、Int型、 Bool型、 String型の値を文字列型に変換する関数が欲しいときを考えます。
そうですね、show関数なんて知らないので自分で作っちゃいましょう。
showInt :: Int -> String
showBool :: Bool -> String
showString :: String -> String
あれ、3つだけ作ってみましたが、もっと欲しくなってきました。
Integer、Float、Double、Rational、Char、、これ全部作らないといけないんですか?
スマートじゃないので、任意の型を引数に取って、文字列を出力するshow関数を作っちゃいましょう。
show :: a -> String
ん、任意の型を許す形で作ってしまいましたが、本当になんでも良いのでしょうか。
例えば、「Int -> Int -> Int
」のような関数の型も立派な型です。
しかし、これは実際には適用することができません。
任意の型と言っても型a
には実は制限があるようです。
これをどうやって表現したら良いのでしょうか・・。
天才なので思いつきました🎉
型の性質を定義した、型のクラスを作ればいいのでは?
ここで出てきたのが型クラスです。
値を文字列型に変換できる性質をクラスで表して、その性質を得るために型をインスタンスに取ればいいのでは。
型クラスを使って、この型aに対する制約を表現することで、今ほしかったものがスマートに定義できるようになりました。
show :: Show a => a -> String
型クラスの調べ方
ある型クラスが、どのようなインターフェースを持ち、どの型がインスタンスであるかを調べる方法があります。
一つはghciを使う方法です。
ghciで: i 型クラス名
とすると、その型クラスの情報をみることができます。
「i」は「info」の「i」でしょうか。
例えばShow型クラスの情報を見てみます。
ghci> :i Show
class Show a where
showsPrec :: Int -> a -> ShowS
show :: a -> String
showList :: [a] -> ShowS
{-# MINIMAL showsPrec | show #-}
-- Defined in ‘GHC.Show’
instance (Show a, Show b) => Show (Either a b)
-- Defined in ‘Data.Either’
instance Show a => Show [a] -- Defined in ‘GHC.Show’
instance Show Word -- Defined in ‘GHC.Show’
instance Show GHC.Types.RuntimeRep -- Defined in ‘GHC.Show’
instance Show Ordering -- Defined in ‘GHC.Show’
instance Show a => Show (Maybe a) -- Defined in ‘GHC.Show’
instance Show Integer -- Defined in ‘GHC.Show’
instance Show Int -- Defined in ‘GHC.Show’
instance Show Char -- Defined in ‘GHC.Show’
instance Show Bool -- Defined in ‘GHC.Show’
...
上の方に書いてある、showsPrec, show, showListがインターフェースとして持っているメソッドです。
それより下にインスタンス一覧が書かかれてあります。
まとめと所感
型クラスについてざっと見てきました。
最初に一言で言った「型の振る舞い定義するもの」という意味が何となくわかってきました。
型クラスは自分で定義することもでき、既存の型をある型クラスのインスタンスにすることもできますが、その辺は長くなるので省略します。
この型クラスという機能、名前や詳細は違えど似たような機能が他の言語でもあったりします。
JavaやTypeScriptのインターフェースや、C++の抽象クラスや、Rustのtrait、DやKotlinやGo2のcontract、Nimのconcept、SwiftのProtocol、などなど(半分くらいちゃんと知らずに適当に言っている部分があるので間違っていたらすみません)
各言語の思想に沿って言語機能を実装する中でも、プログラミングをする上で抽象的にはあると便利な機能だということがわかります。
奥が深そうで面白そうですね。