Haskellの型プログラミング 2.型と種(カインド)

読書メモ Thinking with Types

この記事は,「Thinking with Types」を読んで自分なりに作成したメモです。プログラミングは趣味でやっているので,数学や情報工学上の適切な訳語ではない可能性があります。誤りがありましたら,お知らせいただければ幸いです。

第1部 基礎

第1章 型と代数

第2章 termtypeおよびkind

2.1 カインドシステム

日常的な Haskell プログラムは,termtypeで構成されている。比喩的に言えば,型レベルのプログラミングでは,型とカインドで構成される。種システムとは,型の型システムのことと思えば良い。つまり,カインドとは型の型のようなものである。

魚野: GHCでは,カインドは(型のカインド, アローカインド, 制約カインド,データカインド)の四種類?

2.1.1 型のカインド

型という言葉には複数の意味がある。

広義には,型レベルで存在するもののこと。termでもなければカインドでもない。

狭義には,TYPEと書かれるもの,即ち,型のカインドのことである。歴史的に,TYPE*と表現されていた。TYPE は,IntMaybe Boolのようなもののカインドのことで,実行時に存在するものの種類を区分する役割がある。TYPEでないカインドとしては,例えばパラメタなしのMaybeShowがある。

以降,TYPE(魚野注:原著ではSMALLCAPSで表現) と大文字斜体で表現した場合,TYPEというカインドを表すことにする。

2.1.2 Arrow カインド(Arrow Kinds)

後で調べる: HKTとArrow()を使って作られたカインドの関係は?HKTは高階型とでも訳す?

HKT(Higher-kinded types)とは,型変数を持つものをいう。型変数に具体的なものが詰められたとき,HKTは,単なる TYPE となる。逆にいうと,HKTの型コンストラクタは,TYPE ではない。

HKTの例

Maybe

TYPE パラメタを一つとるので,TYPE → TYPE というカインドを持つ。

MaybeT

TYPE パラメタを二つとるが,同様にパラメタをふたつとるEitherのパラメタが二つとも TYPE であるのと異なり,一つのパラメタはMonadでなければならない。Monadは常にTYPE → TYPEというカインドを持つので,MaybeTが持つカインドは,(TYPE → TYPE ) → TYPE → TYPEとなる。

2.1.3 制約カインド(Constraint Kinds)

カインドは,単に型にだけではなく,全ての型レベルのものに適用される。

例えば,Showの型は,型署名シグネチャとしてShow a ⇒ a → Stringと表現されるが,これは明らかに TYPE ではない。実はShowカインドTYPE ではなく,CONSTRAINT である。

CONSTRAINT は,制約constraintカインド,即ち,型クラスのうち全ての型変数に具体的な型の詰め込みが終わったsaturatedもののカインドである。

2.2 データカインド(Data Kinds)

GHC拡張の-XDataKindsを使うと,TYPECONSTRAINT 及びアロー()を使ったそれらの派生型とはまた別のカインドを使うことができる。

-XDataKindsは,データコンストラクタを型コンストラクタにする。

data Bool = True | False

-XDataXindsが有効になっていれば,上記の宣言は,Boolという新しいカインドと,Boolというカインドを持つ'True'Boolという二つの新しい型を宣言したことにもなる。

このような'True'Falseを,昇格プロモートしたデータコンストラクタ という。

コンストラクタ名の前についているアポストロフィ(')はティックtickと呼ばれ,通常の型コンストラクタと昇格プロモートしたデータコンストラクタを区別するために用いられる。

data Unit = Unit

ティックを使っていれば,上記のような宣言をされたときでも,TYPEというカインドを持つ型コンストラクタUnitと,UNITというカインドを持つプロモートされたデータコンストラクタ'Unitが,型レベルで混在していても明確に区別できる。Maybe Unit型の値を扱うことは正当だが,Maybe 'Unitの値を扱おうとすると,カインドの誤りとして検出される。

データカインドの重要性

Haskellの型レベルプログラミングでのデータカインドの位置付けは,通常のHaskellプログラミングの型の位置付けと同じであり,仮に型レベルプログラミングでデータカインドを欠いていれば,それは通常のプログラミングで動的型付けをしているとの同じことになり,カインドを明示するほど,型の制約について豊富なヒントが得られるようになる。

昇格プロモートしたデータコンストラクタは,実行時に,カインドの誤りとして不具合につながる型の情報を教えてくれる。

ここでは,予め組み込まれた管理者の権限がないと扱えない機能の管理をすることを考えよう。万が一にも非管理者が当該機能を使えるようになってしまうことを避けるため,この制約をビジネスロジックの誤りではなく,型の誤りとして扱うことにし,型システムの力を借りて誤り無きようにしよう。

以下のように,UserType型を定義し,これによって昇格プロモートしたデータコンストラクタを扱うことにする。また,User型を以下のように宣言し,各ユーザが権限トークンを保持できるようにする。加えて,厳密に管理しなければならない機能を実行する際には,管理者権限トークンを必要とする旨を以下のように記述する。

data UserType = User | Admin

data User = User { userAdminToken ::  Maybe (Proxy 'Admin) , ... }

doSensitiveThings :: Proxy 'Admin -> IO()
doSensitiveThings = ...

こうすれば,何か誤って管理者権限トークンなしでdoSensitiveThingsを呼び出すプログラムを記述してしまったとしても,型エラーでコンパイルできないことになる。更に改善した手法(後述のSTトリック等)もある。

2.3 組込みbuilt-in型の昇格プロモート

注:本節では,ライブラリの読み込みが必要である。

import GHC.TypeLits

-XDataKindsを使うと,組込み型(文字列,数,リスト及びタプル)などほとんど全ての型が自動的にカインド昇格プロモートされる(GADTsやその他の普通でない型は昇格プロモートは自動ではできない)。

2.3.1 Symbol

String昇格プロモートすると, SYMBOL というカインドとなる。 SYMBOLに属するものは,文字列ではないが,GHC.TypeLitsで定義されているAppendSymbolプリミティブを使って結合可能である。

> :set -XDataKinds

> :kind "hello"
"hello" :: Symbol

> :kind AppendSymbol
AppendSymbol :: Symbol -> Symbol -> Symbol

> :kind! AppendSymbol "thinking" " with types"
AppendSymbol "thinking" " with types" :: Symbol
= "thinking with types"

SYMBOL の比較も,CompSymbolプリミティブで可能。ただし,Ordering が得られることに注意。この Ordering は,標準ライブラリ PreludeOrdering-XDataKinds によって昇格プロモートされたものである。

> :kind CmpSymbol
CmpSymbol :: Symbol -> Symbol -> Ordering

> :kind! CmpSymbol "sandy" "sandy"
CompSymbol "sandy" "sandy" :: Ordering
= 'EQ

> :kind! CmpSymbol "sandy" "batman"
CompSymbol "sandy" "batman" :: Ordering
= 'GT
2.3.2 自然数

昇格プロモートした数は更に変則的だ。自然数のみが昇格プロモートする(つまり,負の数や分数型,不動小数点型の昇格プロモートはない)。自然数が昇格プロモートしたとき,カインドNAT となる。

文字列の時と同様,GHC.TypeLitsには,NAT リテラルの演算を行うことができる演算子が定義されており,-XTypeOperatorsを使えば利用できる。

> :set -XTypeOperators

> :kind! (1 + 17) * 3
(1 + 17) * 3 :: Nat
= 54

> kind! (128 `Div` 8) ^ 2
(128 `Div` 8) ^ 2 :: Nat
= 256
2.3.3 リスト

リストのデータコンストラクタが昇格プロモートした場合,元のリストの定義とよく似た構造をしている。

data [a] = [] | a: [a]

リストのデータコンストラクタ昇格プロモートすると得られるのは,

  • '[]カインド[A]
  • '(:)カインドA → [A] → [A]  ;中置演算子として使う場合, x ': xs

カインドレベルでの多相性については後述するが,概ね,類推できるような多相性を持つ。

リストのデータコンストラクタは記号なので,-XTypeOperatorsが必要である。

また,[Bool]カインドTYPEであり,式レベルのことを差しているが,'[Bool]カインド[TYPE]であり,型レベルの,BOOL型の要素のリストである。

2.3.4 タプル

タプルについても,'(,)というコンストラクタとなる。

2.4 型レベルの関数 閉じた型族?closed type familliy

-XDataKindsが本当に有用となるのは,閉じた型族?closed type familliyと組み合わせて使う時である。閉じた型族?closed type familliesとは,型レベルの関数と思えば良い。既に,CmpSymbolDivなどを紹介した。

GHCで自分の好きな閉じた型族?closed type familliyを定義することができるが,まずは標準的な関数と,その閉じた型族?closed type famillies版を見比べていこう。

or :: Bool → Bool → Bool
or True  _ = True
or False y = y

type family Or (x :: Bool) (y :: Bool) :: Bool where
  Or 'True  y = 'True
  Or 'False y = y

Mapmapにそっくりの構造である。

map :: (a → b) → [a] → [b]
map _ []       = []
map f (a : as) = f a : map f as

type family Map (x :: a → b) (i :: [a]) :: [b] where
  Map f '[]       = '[]
  Map f (x ': xs) = f x ': Map f xs

もっとも,閉じた型族?closed type familliyは部分適用ができないので,Mapはそれほど有用でははい。

なお,Orの定義の際,そのカインドシグネチャをOR x y :: BOOL → BOOL → BOOLではなく,Or (x :: BOOL) (Y :: BOOL) :: BOOLとした。型族?type familliyカインドは曲者で,::の後に書いたカインドは,型族?type familliyが返す型のカインドであり,型族?type famillyそのもののカインドではないからである。