型クラスについて考える
Haskellの型クラスやインスタンスの仕様について整理するための記事です
型クラスの基礎
Haskellには型クラスというメタ構文があります
Haskellに詳しい方向けの記事となりますが、前提として型クラスとは以下の構造を持つ抽象型です
class Type t where
method::t->t
t
は型引数です
Type
に対するシンボルとして振る舞います
型クラスに定義される関数のシグネチャはプロトタイプ宣言の一種で、メソッドとして機能します
メソッドにはデフォルト実装を与えることも可能です
一方で、型クラスを実装するにはinstance
を使用します
instance Type q where
method x=x
ここで型引数q
はType
の本体となる型です
このような型をインスタンスと呼びます
つまりq
はType
のインスタンスです
このq
はclass
節で宣言したt
に適用される型です
ここで、Haskellユーザーであれば、少し引っ掛かりを覚えるでしょう
そうです、これは一般的な型クラスの解説とは異なります
型クラスとは何か
一般的な解説
型クラスの凡例的な解説としては、インターフェイスとの対比があります
オブジェクト指向においてインターフェイスがクラスに実装されるものであるように、型クラスを実装するという考え方です
これは直感的かつ想像も容易く、実際の運用形態にも適っています
newtype Struct=Init Int
instance Type Struct where
method x=x
データ型が型クラスに所属すると考えれば、両者の関係も明快に理解できます
とはいえ、この解説には欠点もあります
それは以下のような型クラスを想定する場合です
class Types t q where
このような型クラスを実装する場合には、一般的に以下の類のデータ構造を対象とします
newtype Struct t=Init t
instance Types Struct o where
一方で、最初に定義したType
に対しては、以下のような実装も可能です
instance Type (Struct o) where
両者はどのように異なるのでしょうか
型引数o
に具体的な型を指定する必要がないのは何故でしょうか
インターフェイスとしての解釈では、この点を上手く説明できません
更に、Types
に対しては以下の実装も許容されます
instance Types Int String where
この場合Int t
のような特殊な型が存在するのでしょうか?
無論そんなことはありません
これらはいずれもHaskellで標準的な型クラスの振る舞いです
では、型クラス、ひいてはインスタンスとはどんな役割を持った型なのでしょうか
そのヒントが、冒頭に挙げた
instance Type q where
に示されています
制約としての型クラス
ここでは、型クラスの見方を根本から変更します
即ち、型クラスはインターフェイスではなく、型引数に対する制約と等価なテンプレートであるという捉え方です
制約としての名前
型クラスとはつまりある制約を表すラベルです
このことは、型クラスの振る舞いが関数の型引数に対して指定する型制約と等価な性質からも見て取れます
function::Types x y=>x y->y
class Types t q where
method::t q->q
ここでfunction
におけるTypes x y
という型制約は型クラスにおけるTypes t q
に相当します
method
に実装を与えることで、これをfunction
に代入することができます
instance Types Sample x where
method (Sample v)=v
function=method
つまり型クラスにはプロトタイプ宣言のシグネチャに型制約があったとして、それがどのような構成の型に適用されるかを規定する役割があるのです
class Types t q where
--Typesの制約を型引数t及びqに適用する
冒頭でも紹介したように、型クラスとはメタ構文です
t
とq
は文字通り関数の引数に相当する型の引数であり、ここにはどんな型でも指定できます
ただし、型クラスは原則としてプロトタイプの集合です
メソッドとして運用するには実際に制約を適用した時に顕現する性質、すなはち実装を定義する必要があります
ここで登場するのがinstance
です
制約がもたらす性質
型クラスがメタ的なテンプレートであれば、インスタンスはそれに対する約束事です
型クラスが制約を適用する型の構造を決め、インスタンスがその構造を対象に操作を規定します
よってインスタンスに型を指定する役割はありません
具体的な型を実装する必要は本来ないことになります
class Types a b where
method::a->b->(a,b)
instance Types x y where
method x y=(x,y)
ここでは、型引数x
とy
に与えられる全ての型を対象にmethod
の振る舞いを規定しています
しかし、instance
で型を指定しないならば、わざわざ定義と実装を分ける意味はありません
ここに型クラスでサポートされるデフォルト実装の本質が見出されます
class Types a b where
method::a->b->(a,b)
method x y=(x,y)
デフォルト実装は型クラスでの定義とインスタンスでの実装の二段階を省略するための構文です
そしてここまで見た通り、型クラス側の型引数a
とb
はTypes
に対して等価な要素であることが分かります
型構造の謎
さて、a
とb
が等価であるならば、まだ未解決の謎があります
それが先んじて例示した
instance Types Struct o where
instance Type (Struct o) where
の例です
Types
の例では特に型と型引数が個別に宣言されています
しかしStruct
はここでは本来Struct o
という構成のデータ型です
なぜ、Types a b
のa
とb
それぞれに型を分割できるのでしょうか
型のカリー化
この疑問に対する回答は単純です
それは、Haskellが型の宣言をカリー化(部分適用)できるからです
型の部分適用は主にtype
構文の使用時に行われます
data Struct t q=Init t q
type Curry=Struct Int
ここでCurry
はInt
を部分適用したStruct Int q
のエイリアスです
Curry
を宣言すると暗黙的にStruct Int q
と解析されます
このInt
を仮にデータ型を指定しない形に書き換えれば(Struct t) q
となるでしょう
これは型引数に対して可能な操作で、型クラスの型引数に対して型を指定するinstance
節であれば、この操作が利用できるのも自然なことと言えます
data Struct t q=Init t q
instance Types (Struct t) q where
つまり
instance Type (Struct o) where
は、この部分適用を利用した宣言となります
newtype Struct t=Init t
instance Type (Struct o) where
部分適用しない場合は以下のように定義します
type Curry=Struct
Struct t
であれば、ここで型引数t
が余ります
この余ったt
を型クラスに適用した形が以下の宣言となります
newtype Struct t=Init t
instance Types Struct t where
型のカリー化を考えることで、残された謎も説明することができました
継承の本質
ここまでで、関数における型制約と型クラスにおける型定義とが同一の宣言であることを見てきました
型クラスが制約のラベルであり、インターフェイスとは性質が異なるとなれば、問題となるのが一般的な解説における継承の立場です
表題の通り、Haskellでは一般的な意味での実装の継承は適切な表現ではないことになります
では型クラスで継承と説明される手続きの正体は何でしょうか
継承は制約のエイリアス
継承とはすなはち型クラスに属する型引数に対する制約です
これは継承の書式が関数における型制約と同一であることからも分かります
class (Num x,Show y)=>Type x y where
method::x->y->(x,y)
instance Type Int String where
method x y=(x,y)
function::(Num a,Show b)=>Type a b=>a->b->(a,b)
function=method
型制約はインスタンスにも指定できます
instance (Num a,Show b)=>Type a b where
method x y=(x,y)
型クラスに特に型制約の指定がない場合はインスタンスで固有の型制約を定義することが可能です
この場合はインスタンスが型クラスを継承することになるのでしょうか?
しかしそのような表現に出逢うことはありませんね
何故ならこれは単なる型制約の適用例に過ぎないからです
継承と聞くと何か特別な手続きのように捉えられますが、あくまでHaskellで汎用的な構文ということになります
しかし、こうなると一体インスタンスとは何者なのでしょうか
ただ型クラスにデータ型を所属させるような機能でないことは明らかです
何故なら型指定の義務は元々負っていないためです
インスタンスはパターンマッチ
結論から入ると、これは一種のパターンマッチと解釈できます
instance
の本質的な性質は、型クラスに属する型引数に与えられるあらゆる型の内、特定の条件を満たした型に対してメソッドの固有の振る舞いを対応させるためのパターンマッチの一種です
ある型をインスタンスに対応させる場合、それは型クラスを後から実装しているのではなく、単にパターンを追加しているにとどまります
型クラスとインスタンス
型クラスとインスタンスは明確に固有の役割を有した抽象化構文の一つです
日常的な使用ではその本質を意識する必要はありませんが、少なくとも型クラスがただのインターフェイスではないという点を留意するだけでも、型の理解度が異なってくるでしょう
結果的に型クラスを通じてHaskellの言語仕様の深淵を覗く回となりました
検証用コード
本記事の執筆に当たり検証用のサンプルとしたプログラムを添付します
{-#LANGUAGE IncoherentInstances#-}
{-#LANGUAGE UndecidableInstances#-}
{-#LANGUAGE DeriveAnyClass#-}
main::IO ()
main = sequence_ $ [putStrLn.show.fst
.f 0.1::String->IO (),putStrLn]
<*>((:[])
.(test::(Sub Int String)->String)
.Make 1 $ "Success!")
class Test a b where
fn::a b->b
class (Num x,Show y)=>Value x y where
f::x->y->(x,y)
instance Value Int String where
f x y=(x,y)
comment::(Num a,Show b)=>Value a b=>a->b->(a,b)
comment=f
instance (Num a,Show b)=>Value a b where
f x y=(x,y)
newtype Sample t=Init t
instance Test Sample t where
fn::Sample t->t
fn (Init i)=i
data Module x=Mod x deriving (Test Sample)
data Sub a b=Make a b
type M=Sub Int
instance Test (Sub x) y where
fn (Make a b)=b
test::Test a b=>a b->b
test=fn
0.1
Success!
Discussion