🐷

型クラスについて考える

に公開

Haskellの型クラスやインスタンスの仕様について整理するための記事です

型クラスの基礎

Haskellには型クラスというメタ構文があります
Haskellに詳しい方向けの記事となりますが、前提として型クラスとは以下の構造を持つ抽象型です

class Type t where
 method::t->t

tは型引数です
Typeに対するシンボルとして振る舞います
型クラスに定義される関数のシグネチャはプロトタイプ宣言の一種で、メソッドとして機能します
メソッドにはデフォルト実装を与えることも可能です
一方で、型クラスを実装するにはinstanceを使用します

instance Type q where
 method x=x

ここで型引数qTypeの本体となる型です
このような型をインスタンスと呼びます
つまりqTypeのインスタンスです
このqclass節で宣言した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に適用する

冒頭でも紹介したように、型クラスとはメタ構文です
tqは文字通り関数の引数に相当する型の引数であり、ここにはどんな型でも指定できます
ただし、型クラスは原則としてプロトタイプの集合です
メソッドとして運用するには実際に制約を適用した時に顕現する性質、すなはち実装を定義する必要があります
ここで登場するのがinstanceです

制約がもたらす性質

型クラスがメタ的なテンプレートであれば、インスタンスはそれに対する約束事です
型クラスが制約を適用する型の構造を決め、インスタンスがその構造を対象に操作を規定します
よってインスタンスに型を指定する役割はありません
具体的な型を実装する必要は本来ないことになります

class Types a b where
 method::a->b->(a,b)

instance Types x y where
 method x y=(x,y)

ここでは、型引数xyに与えられる全ての型を対象にmethodの振る舞いを規定しています
しかし、instanceで型を指定しないならば、わざわざ定義と実装を分ける意味はありません
ここに型クラスでサポートされるデフォルト実装の本質が見出されます

class Types a b where
 method::a->b->(a,b)
 method x y=(x,y)

デフォルト実装は型クラスでの定義とインスタンスでの実装の二段階を省略するための構文です
そしてここまで見た通り、型クラス側の型引数abTypesに対して等価な要素であることが分かります

型構造の謎

さて、abが等価であるならば、まだ未解決の謎があります
それが先んじて例示した

instance Types Struct o where
instance Type (Struct o) where

の例です
Typesの例では特に型と型引数が個別に宣言されています
しかしStructはここでは本来Struct oという構成のデータ型です
なぜ、Types a babそれぞれに型を分割できるのでしょうか

型のカリー化

この疑問に対する回答は単純です
それは、Haskellが型の宣言をカリー化(部分適用)できるからです
型の部分適用は主にtype構文の使用時に行われます

data Struct t q=Init t q
type Curry=Struct Int

ここでCurryIntを部分適用した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の言語仕様の深淵を覗く回となりました

検証用コード

本記事の執筆に当たり検証用のサンプルとしたプログラムを添付します

https://onecompiler.com/haskell/43uv8ykj6

{-#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