Haskellの型プログラミング 2.型と種(カインド)
読書メモ Thinking with Types
この記事は,「Thinking with Types」を読んで自分なりに作成したメモです。プログラミングは趣味でやっているので,数学や情報工学上の適切な訳語ではない可能性があります。誤りがありましたら,お知らせいただければ幸いです。
第1部 基礎
第2章 式,型および種
2.1 種システム
日常的な Haskell プログラムは,式と型で構成されている。比喩的に言えば,型レベルのプログラミングでは,型と種で構成される。種システムとは,型の型システムのことと思えば良い。つまり,種とは型の型のようなものである。
魚野: GHCでは,種は(型の種, アロー種, 制約種,データ種)の四種類?
2.1.1 型の種
型という言葉には複数の意味がある。
広義には,型レベルで存在するもののこと。式でもなければ種でもない。
狭義には,TYPE
と書かれるもの,即ち,型の種のことである。歴史的に,TYPE
は*
と表現されていた。TYPE
は,Int
やMaybe Bool
のようなものの種のことで,実行時に存在するものの種類を区分する役割がある。TYPE
でない種としては,例えばパラメタなしのMaybe
やShow
がある。
以降,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 は,制約種,即ち,型クラスのうち全ての型変数に具体的な型の詰め込みが終わったものの種である。
2.2 データ種(Data Kinds)
GHC拡張の-XDataKinds
を使うと,TYPE や CONSTRAINT 及びアロー(→
)を使ったそれらの派生型とはまた別の種を使うことができる。
-XDataKinds
は,データコンストラクタを型コンストラクタにする。
例
data Bool = True | False
-XDataXinds
が有効になっていれば,上記の宣言は,Bool
という新しい種と,Bool
という種を持つ'True
と'Bool
という二つの新しい型を宣言したことにもなる。
このような'True
と'False
を,昇格したデータコンストラクタ という。
コンストラクタ名の前についているアポストロフィ('
)はティックと呼ばれ,通常の型コンストラクタと昇格したデータコンストラクタを区別するために用いられる。
例
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 組込み型の昇格
注:本節では,ライブラリの読み込みが必要である。
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
は,標準ライブラリ Prelude
の Ordering
が -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 型レベルの関数 閉じた型族?
-XDataKinds
が本当に有用となるのは,閉じた型族?と組み合わせて使う時である。閉じた型族?とは,型レベルの関数と思えば良い。既に,CmpSymbol
やDiv
などを紹介した。
GHCで自分の好きな閉じた型族?を定義することができるが,まずは標準的な関数と,その閉じた型族?版を見比べていこう。
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
Map
もmap
にそっくりの構造である。
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
もっとも,閉じた型族?は部分適用ができないので,Mapはそれほど有用でははい。
なお,Or
の定義の際,その種シグネチャをOR x y :: BOOL → BOOL → BOOL
ではなく,Or (x :: BOOL) (Y :: BOOL) :: BOOL
とした。型族?の種は曲者で,::
の後に書いた種は,型族?が返す型の種であり,型族?そのものの種ではないからである。