Haskellの型クラスについて

この記事は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関数を適用してみるとご覧の通り。

また、show関数の型定義は以下のようになっています。

「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関数なんて知らないので自分で作っちゃいましょう。

あれ、3つだけ作ってみましたが、もっと欲しくなってきました。
Integer、Float、Double、Rational、Char、、これ全部作らないといけないんですか?

スマートじゃないので、任意の型を引数に取って、文字列を出力するshow関数を作っちゃいましょう。

ん、任意の型を許す形で作ってしまいましたが、本当になんでも良いのでしょうか。

例えば、「Int -> Int -> Int」のような関数の型も立派な型です。
しかし、これは実際には適用することができません。

任意の型と言っても型aには実は制限があるようです。
これをどうやって表現したら良いのでしょうか・・。

天才なので思いつきました🎉
型の性質を定義した、型のクラスを作ればいいのでは?

ここで出てきたのが型クラスです。
値を文字列型に変換できる性質をクラスで表して、その性質を得るために型をインスタンスに取ればいいのでは。

型クラスを使って、この型aに対する制約を表現することで、今ほしかったものがスマートに定義できるようになりました。

型クラスの調べ方

ある型クラスが、どのようなインターフェースを持ち、どの型がインスタンスであるかを調べる方法があります。

一つはghciを使う方法です。
ghciで: i 型クラス名とすると、その型クラスの情報をみることができます。
「i」は「info」の「i」でしょうか。

例えばShow型クラスの情報を見てみます。

上の方に書いてある、showsPrec, show, showListがインターフェースとして持っているメソッドです。

それより下にインスタンス一覧が書かかれてあります。

まとめと所感

型クラスについてざっと見てきました。
最初に一言で言った「型の振る舞い定義するもの」という意味が何となくわかってきました。

型クラスは自分で定義することもでき、既存の型をある型クラスのインスタンスにすることもできますが、その辺は長くなるので省略します。

この型クラスという機能、名前や詳細は違えど似たような機能が他の言語でもあったりします。

JavaやTypeScriptのインターフェースや、C++の抽象クラスや、Rustのtrait、DやKotlinやGo2のcontract、Nimのconcept、SwiftのProtocol、などなど(半分くらいちゃんと知らずに適当に言っている部分があるので間違っていたらすみません)

各言語の思想に沿って言語機能を実装する中でも、プログラミングをする上で抽象的にはあると便利な機能だということがわかります。

奥が深そうで面白そうですね。

 

参考