#はじめに
haskellを趣味で勉強し始めて間もないのですが最初の壁が「型と型コンストラクタと型変数と型クラス」の違いがよくわからんというものでした。
なので今回はこいつらについて整理してみたいと思います。
型と型コンストラクタと型変数と型クラス
初めて本を読んだとき混乱したのが「型と型コンストラクタと型変数と型クラス」です。
まず最初にこいつらの関係を図にしてみたのが次です。型クラスだけは後から出します。
(図に書いてある*とか*->*は本当は「型コンストラクタのカインド」というものですがわかりやすさのために許してください)
カインド
カインドは英語でkindと書いて「種類」という意味がある単語です。
要するに目の前にある型と型コンストラクタの種類を表現するためのものです。
基本的に「*」と「->」の組み合わせだけで表現されます(型クラスだけは特殊な「Constraint:制約」というカインドを持ちます)。
さて、これを踏まえてまず型と型コンストラクタの違いをはっきりさせます。
結論だけ言ってしまえば、
「型」とは、カインドが「*」のもの。
「型コンストラクタ」とは任意の*と->からなるカインドを持つもの(*,*->*, *->*->*など)。
です。
注意としては「型は型コンストラクタ」に含まれる({型}⊂{型コンストラクタ})ということですかね。
型・型コンストラクタ・型変数
カインドという便利な概念を導入したところで、型・型コンストラクタ・型変数に関して、まとめていきます。
(1/3) 型
型は「そのデータの種類」です。
たとえば、
- 12 の型は Int
- True の型は Bool
というように具体的なデータそれぞれがどんな種類のデータなのかを示しています。
型と聞いておそらく多くの人が普通に想像するもので、オブジェクト指向のそれと変わりません。
これらのInt, Boolと言った単体でデータの種類を表すものをカインドでは「*」と表現します。
(2/3) 型コンストラクタ
型コンストラクタは「型を作るための型の前段階」です。
型コンストラクタそのものは型であったりなかったりします。
これは例をあげた方が分かりやすいかもしれません。
型コンストラクタは名前の通り「型をコンストラクト」するためのものなので、具体的な型を入れ込んでやると型になります。
こういう意味で、型はそれ自身単体で型となり得る特別な型コンストラクタというわけですね。
ポイントは***「型コンストラクタは型と組み合わせることで型になる」***ということです。
(3/3) 型変数
型変数は複数の型を統一的に扱うためのものです。数学でいうところの変数と同じですね。
$x$という変数に$1, 2, 3.14$ など様々な数を代入できたのと同じように、a
という型変数にはいろんな型を入れることができます。
関数を作るときに複数の型に対して同じ動きをしてほしいときがあります。
たとえば、恒等関数 id にはどんな型のデータを渡してもそのまま返してほしいと思うはずです。
実際、id は、
Prelude> id 1 ::Int
1
Prelude> id 'a' :: Char
'a'
Prelude> id "Hello world" :: String
"Hello world"
という挙動を示すように最初から組み込まれています。
上の例のように、id という関数にはどんな型の値を渡してもそれをそのまま返してくれます。これは人間にとっては自然ですが、型を気にするコンパイラからしたら迷惑な話です。
そこで、複数の型に対応するためにあるのが型変数です。
Prelude> :type id
id :: a -> a
上のように:type
を使うとid の型を調べることができます。このときに出てくる、a
というのが型変数です。
a
にはInt, Char, String
など様々な型が入れるわけですね。
注意 ~型変数のカインドは*だけとは限らない~
上の例を見ると型変数は型(*)だけが入るのではと思うと思います(というか、ぼくは絶対そうだと思いました)。
しかし、少しややこしいことに型変数のカインドは*だけとは限らないようです。
たとえば、fmap
という関数は、
Prelude> :type fmap
fmap :: Functor f => (a -> b) -> f a -> f b
という型を持ちます。
ここに出てくるf
という型変数のカインドは*->*なので、カインド*ではない型変数の例になっていますね。
型クラス
続いて型クラスについてまとめていきたいと思います。
型クラスは複数の型に対して同様の関数を定義するための仕組みです。さっき出てきたid
も型クラスを用いて定義されています。
次の画像はNum
型クラスの定義を:info
で表示したものです。
オブジェクト指向のインスタンスと少し違うのは、型クラスにはすでに定義されてる型コンストラクタをインスタンスとして追加するという点でしょうか。
Int, Integer, Float, Double
などは人間にとっては「数」というくくりでまとめて考えられるものです。
これらには共通して足し算「+」が定義されていてほしいと思うのも自然なことです。
そのためにこれらの型をNum
という型クラスでまとめて管理し、Num
型クラスのメソッドとして(+)を定義します。
型クラスもやはりカインド(*)だけとは限らず、様々なカインドを持つ型コンストラクタに定義することができます。
Functor
の使い方に関してはまた改めて色々試してみたいですね(定義見る限り圏論に出てくる関手と同じものに見えますが、具体的にどう役立つのか楽しみです)。
おわりに
記事を書いている自分は理解がふかまりました。
でも、やっぱりこういうややこしい概念の理解は自分で手を動かす方が良い気もしますね。
次はどんなテーマで書くかお楽しみに。