型クラスとインスタンスの宣言(和訳)
型クラス宣言
この節と次の節ではGHCの型クラスの拡張について説明する.型クラス:デザイン空間を探る (Simon Peyton Jones, Mark Jones, Erik Meijer)にはたくさんの背景がある. 複数パラメータの型クラス
MultiParamTypeClasses
ConstrainedClassMethodを含む
6.8.1以降
複数のパラメータを持つ型クラスを定義できるようにする.
MultiParamTypeClasses拡張下では,複数のパラメータを持つ型クラスが許可される.例えば:
code: class1.hs
class Collection c a where
union :: c a -> c a -> c a
...
型クラス宣言での親クラス
FlexibleContexts
6.8.1以降
クラス宣言でのコンテキストで複雑な制約を使用できるようにする.
Haskell 98では,クラス宣言での(スーパークラスを導入する)コンテキストは単純でなければならない.つまり,各述語は型変数に適用される型クラスで構成されている必要がある.FlexibleContexts拡張はこの制限を取り除く.そのため,型クラス宣言内のコンテキストに対する唯一の制限は,型クラス階層が非循環的でなければならないということだけである.したがって,以下の型クラス宣言は問題ない:
code: class2.hs
class Functor (m k) => FiniteMap m k where
...
class (Monad m, Monad (t m)) => Transform t m where
lift :: m a -> (t m) a
Haskell 98のように,型クラス階層は非巡回的でなければない.ただし,「非巡回的」の定義には親クラスの関係のみが含まれる.たとえば,以下は問題ない:
code: class3.hs
class C a where
op :: D b => a -> b -> b
class C a => D a where ...
ここでは,CはDの親クラスであるが,型クラスCのメソッドopがDに言及するのは問題ない(DがCの親クラスになるのはダメである).
制約種を追加する拡張を使えば,より面白い親クラスの定義を書くことができる.この場合,親クラスの巡回性検査はさらに自由度がある.たとえば,以下は問題ない:
code: class4.hs
class A cls c where
meth :: cls c => c -> c
class A B c => B c where ...
型シノニムを右辺に展開し,(C以外の)型クラスの使用を親クラスに展開した後で,Cがコンテキスト内で構文的に発生しない場合は,型クラスCの親クラスのコンテキストが許される.
メソッドのコンテキストのついた型シグネチャ
ConstrainedClassMethods
6.8.1以降
個々のクラスメソッドに対するさらなるコンテキストの定義を許可する.
Haskell 98は型クラスのメソッドの型シグネチャで型クラスの型変数にコンテキストをつけることを禁止している.例:
code: class5.hs
class Seq s a where
elem :: Eq a => a -> s a -> Bool
elemの型はHaskell 98では不正である.なぜなら,型クラスの型変数(この場合はa)のみを制限するコンテキストEq aを含んでいるからである.より正確には,型クラスのメソッドの型シグネチャの制約は次の場合に拒否される:
制約が少なくとも1つの型変数に言及している.だからこれは許可される.
code: class6.hs
class C a where
op1 :: HasCallStack => a -> a
op2 :: (?x::Int) => Int -> a
言及されている型変数がすべて型クラス宣言によって束縛されており,ローカルに量化されているものがない.例:
code: class7.hs
class C a where
op3 :: Eq a => a -> a -- Rejected: constrains class variable only
op4 :: D b => a -> b -- Accepted: constrains a locally-quantified variable b
op5 :: D (a,b) => a -> b -- Accepted: constrains a locally-quantified variable b
GHCはConstrainedClassMethods拡張でこの制限を取り除く.この制限はそもそもかなり頭が悪いものなので,ConstrainedClassMethods拡張はMultiParamTypeClasses拡張に含まれている.
デフォルトのメソッドの型シグネチャ
DefaultSignature
7.2.1以降
クラス定義内でデフォルトのメソッドの型シグネチャの定義を許可する.
Haskell 98では,型クラスを宣言するときにデフォルト実装を定義することができる.
code: class8.hs
class Enum a where
enum = []
enumメソッドの型は[a]である.これはデフォルトのメソッドの型でもある.DefaultSignatures拡張を使用してこの制限を解除し,デフォルトのメソッドに別の型を指定することができる.あなたがGHC.GenericsのGEnumクラスのgenumメソッドを用いてジェネリックな列挙の実装を書いた場合,そのジェネリックな実装を使用するデフォルト実装を書くことができる:
code: class9.hs
class Enum a where
default enum :: (Generic a, GEnum (Rep a)) => a enum = map to genum
型シグニチャがデフォルト実装にのみ適用されることを示すために,defaultキーワードを再利用する.Enumクラスのインスタスを定義するときに,元の型[a]のenumは通常通り定義する必要がある.しかし,空のインスタンスを定義したとき,デフォルトの実装(map to genum)が使われ,メソッドは(Generic a, GEnum (Rep a)) => [a]で型検査される.
型クラスのデフォルト実装の型シグネチャは,対応するメインのメソッドの型シグネチャと同じ形式にする必要がある.そうでなければ,型検査器はその型クラスの定義を拒否する.「同じ形式をとる」とは,デフォルト実装の型シグネチャとメインのメソッドの型シグネチャの相違点がコンテキストのみだということである.したがって,メソッドbarがあるとき:
code: class10.hs
class Foo a where
bar :: forall b. C => a -> b -> b
barのデフォルト実装の型シグネチャは次の形式を取る.
code: class11.hs
default bar :: forall b. C' => a -> b -> b
Cは C 'と違ってもよいが,型シグネチャの右辺は一致する必要がある.なぜこの要求が課されるかというと,DefaultSignaturesを使用する型クラスに対して空のインスタンスを宣言した場合,GHCは暗黙的に次のようにデフォルト実装を埋めるからである:
code: class12.hs
instance Foo Int where
bar = default_bar @Int
ただし,@Intという表記はデフォルト実装のbar :: forall b. C' => a -> b -> b内 のbを実体化するために型適用(TypeApplication拡張)を用いたものである.この型適用が機能するためには,barのデフォルト実装の型シグネチャは,デフォルト実装以外の型シグネチャと同じ順序で型変数を使う必要がある.しかし,CとC 'が同じである必要はない(例えば,この例と同様の上記のEnumの例を参照せよ).
この例をさらに説明するならば,barのデフォルト実装の型シグネチャの右側は,forall b . a -> b -> bとα同値なものでなければならない(ここでaは型クラス自身によって束縛されているので,メソッドの型シグネチャでは自由変数である).したがって,以下も許容されるデフォルト実装の型シグネチャになる:
code: class13.hs
default bar :: forall x. C' => a -> x -> x
しかし,次は許されない(自由変数aが間違った場所にあるため):
code: class14.hs
default bar :: forall b. C' => b -> a -> b
型変数bを具象型Intと一致させることはできないので,以下も同様に許されない:
code: class15.hs
default bar :: C' => a -> Int -> Int
ただし,a -> Int -> Intはforall bの 直接的なインスタンスなので,class15.hsだけは特別な言及に値する.それでも以下のようなデフォルト実装の型シグネチャを書くことができるが,そのためには型同値を使用する必要がある:
code: class16.hs
default bar :: forall b. (C', b ~ Int) => a -> b -> b
GHCではジェネリックプログラミングをシンプルにするためにDefaultSignaturesを使う.
Null型クラス
NullaryTypeClasses
7.8.1以降
パラメータなしで型クラスの定義を許可する.この拡張はMultiParamTypeClassesに統合された.
MultiParamTypeClassesでは,Null(パラメータなし)型クラスが有効になる.歴史的には,それらは(現在は非推奨の)NullaryTypeClassesで有効にされていた.使用可能なパラメータがないため,Null型クラスのインスタンスは最大で1つである.Null型クラスは,型シグネチャの仮定(Riemann予想への依存など)をドキュメント化するため,あるいははプログラムでグローバルに構成可能な設定を追加するために使用されることがある.例えば:
code: class17.hs
class RiemannHypothesis where
assumeRH :: a -> a
-- Deterministic version of the Miller test
-- correctness depends on the generalised Riemann hypothesis
isPrime :: RiemannHypothesis => Integer -> Bool
isPrime n = assumeRH (...)
isPrimeの型シグネチャは,その正確さが証明されていない仮定に依存していることをユーザに知らせる.この機能が使用されている場合,次のようにユーザは依存関係を確認する必要がある:
code: class18.hs
instance RiemannHypothesis where
assumeRH = id
関数従属
インスタンス宣言
インスタンス宣言は次の形式を取る:
code: ins1.hs
instance (assertion1, ..., assertionn) => classname type1 ... typem where ...
=>の前の部分はコンテキストであり,=>の後の部分はインスタンス宣言の先頭である.
インスタンス解決
GHCが制約C Int Boolを解決しようとすると,インスタンス宣言の先頭をインスタンス化することによって,すべてのインスタンス宣言を制約と照合しようとする.次の宣言を考えてみよう:
code: ins2.hs
instance context1 => C Int a where ... -- (A)
instance context2 => C a Bool where ... -- (B)
GHCのデフォルトの振る舞いでは,ただ1つのインスタンスが,解決しようとしている制約と一致しなければならない.たとえば,制約C Int Boolはインスタンス(A)と(B)の両方に一致するため,拒否される.一方で,C Int Charは(A)とのみ一致するので,(A)が選択される.
なお,以下に注意すること:
マッチング時,GHCはインスタンス宣言のコンテキスト(context1など)を考慮しない.
(宣言(A)と(B)の両方を考えると)重複の可能性があることは問題ない.特定の制約が複数のインスタンス宣言に一致する場合にのみエラーになる.
インスタンス解決規則を緩めるフラグについては,インスタンスの重複を参照せよ.
インスタンス宣言先頭の制約の緩和
TypeSynonymInstances
6.8.1以降
型シノニムの型クラスインスタンスの定義を許可する.
FlexibleInstances
TypeSynonymInstances拡張を含む.
6.8.1以降
インスタンス宣言の先頭に任意のネストした型を持つ型クラスインスタンスの定義を許可する.
Haskell 98では,インスタンス宣言の先頭はC (T a1 ... an)の形式でなければならない .ここでCはクラス,Tは型コンストラクタ,そしてa1 ... anはdistinctな型変数である.複数パラメータの型クラスの場合,この規則はインスタンス宣言の先頭の各パラメータに適用される(1つだけがこの形式を持ち,他のものが型変数であればおそらく大丈夫だが,現時点での規則は上記のとおりである).
GHCはこの規則を2つの方法で緩和する.
TypeSynonymInstances拡張は,インスタンス宣言の先頭に型シノニムを使用することができる.いつものように,型シノニムを使用することは,型シノニム定義の右辺を書くことの単なる略記である.例えば:
code: ins3.hs
type Point a = (a,a)
instance C (Point a) where ...
は合法である.このインスタンス宣言は以下と同等である.
code: ins4.hs
instance C (a,a) where ...
いつものように,型シノニムは完全に適用されなければならない.たとえば,次のように書くことはできない.
code: ins5.hs
instance Monad Point where ...
FlexibleInstances拡張は,インスタンス宣言の先頭が任意のネストされた型に言及するのを可能にする.たとえば,これは合法的なインスタンス宣言になる.
code: ins6.hs
instance C (Maybe Int) where ...
インスタンスの重複に関する規則も参照せよ.
FlexibleInstances拡張はTypeSynonymInstancesを含む.
ただし,インスタンス宣言はインスタンス決定の規則に準拠している必要がある.インスタンス終了の規則を参照せよ.
インスタンス宣言でのコンテキストの規則の緩和
Haskell 98では,インスタンス宣言における型クラス宣言はC aの形式でなければならない.ここで,aはインスタンス宣言の先頭に現れる型変数である.
FlexibleContexts拡張は,この規則に加え,対応する型シグネチャの規則を緩和する(型シグネチャのコンテキストを参照せよ).具体的には,FlexibleContextsは,インスタンス宣言のコンテキストで(well-kindedな)(C t1 ... tn)という形式の型クラス制約を書けるようにする.
この拡張は,インスタンスのコンテキストにおける等価制約には影響しない.それらはTypeFamiliesまたはGADTsによって使えるようになる.
ただし,インスタンス宣言はインスタンス決定の規則に準拠している必要がある.インスタンス決定の規則を参照せよ.
インスタンス決定の規則
UndecidableInstances
6.8.1以降
型検査が終了しない可能性があるインスタンスの定義を許可する.
FlexibleInstancesおよびFlexibleContextsに関係なく,インスタンス宣言は,インスタンスの解決が確実に終了するようにするためのいくつかの規則に従う必要がある.UndecidableInstancesを使用してその制限を解除できる(決定不能なインスタンスを参照せよ).
規則は次のとおりである.
Paterson条件: 型クラス制約における各制約(C t1 ... tn)について
先頭よりも多く制約内に出現する型変数がない
制約には,先頭よりも少ないコンストラクタと変数(まとめて繰り返しを数える)がある.
制約が型関数について言及していない.型関数適用は原則として任意のサイズの型に拡張することができるため,手に負えなくなる.
カバレッジ条件:型クラス宣言における⟨tvs⟩_left -> ⟨tvs⟩_rightというような各関数従属について,S(⟨tvs⟩_right)内のすべての型変数がS(⟨tvs⟩_left)に登場しなければならない.なお,Sは型クラス宣言の各型変数と対応するインスタンス宣言の先頭の型変数への代入写像のことである.
これらの制限により,インスタンスの解決が確実に終了する.各(演繹)推論のステップにより,少なくともコンストラクタ1つ分だけ問題が小さくなる.これらの制限の理由に関する多くの背景は制約処理規則による関数従属についてに載っている. たとえば,これらは問題ない.
code: ins6.hs
instance C Int a -- Multiple parameters instance Eq (S a) -- Structured type in head -- Repeated type variable in head
instance C4 a a => C4 a a instance Stateful (ST s) (MutVar s)
-- Head can consist of type variables only
instance C a
instance (Eq a, Show b) => C2 a b
-- Non-type variables in context
instance Show (s a) => Show (Sized s a)
instance C2 Int a => C3 Bool a instance C2 Int a => C3 a b しかし,以下はエラーになる.
code: ins7.hs
-- Context assertion no smaller than head
instance C a => C a where ...
-- (C b b) has more occurrences of b than the head
instance C b b => Foo b where ... deriving句によって生成されたインスタンスにも同じ制限が適用される.したがって,以下は問題ない.
code: ins8.hs
data MinHeap h a = H a (h a)
deriving (Show)
なぜなら,以下のようにインスタンスが導出されるからである.
code: ins9.hs
instance (Show a, Show (h a)) => Show (MinHeap h a)
これは上記の規則に従う.
上記の規則で許可されている便利なイディオムは次のとおりである.インスタンス宣言の重複を許可している場合は,より具体的なものが当てはまらない場合に適用される「デフォルトインスタンス」宣言を使用すると非常に便利である.
code: ins10.hs
instance C a where
op = ... -- Default
決定不能なインスタンス
場合によっては,インスタンス決定規則のそれらでさえも面倒になる.したがって,GHCではもっと自由な規則を試すことができる.実験的な拡張UndecidableInstancesを使用すると,Paterson条件とCoverage条件(インスタンスの決定規則で説明)の両方が解除される.インスタンスの探索の終了は,固定深さの再帰スタックを使用することによって保証される.スタックの深さを超えると,一種のバックトレースが発生する.また,-freduction-depth =⟨n⟩でスタックの深さを増やすことができるが,デフォルトのスタック深さを超える必要がある場合は,-freduction-depth = 0でスタック深さの指定を無効にすることがおそらく最善である.あなたのプログラムが必要とする正確なスタック深さは,あなたのコードの細部に依存し,そしてそれはGHCのマイナーリリース間で変わる可能性がある.リリースされたコードに対する最も安全な策は,それが有限時間で確実にコンパイルされる確信があるならば,単にスタックの深さの検査を無効にすることだ.
たとえば,「クラスシノニム」を使うために次のようにしたいことがある.
code: ins11.hs
class (C1 a, C2 a, C3 a) => C a where { }
instance (C1 a, C2 a, C3 a) => C a where { }
これにより,短い型シグネチャを書くことができる.
code:ins12.hs
f :: (C1 a, C2 a, C3 a) => ...
の代わりに
code: ins13.hs
f :: C a => ...
のように書ける.
関数従属に対する制限は特に面倒だ.インスタンス宣言の先頭に現れないコンテキストの中で型変数を導入したくなるが,これは通常の規則では排除されている.例えば,
code: ins14.hs
class HasConverter a b | a -> b where
convert :: a -> b
data Foo a = MkFoo a
instance (HasConverter a b,Show b) => Show (Foo a) where
show (MkFoo value) = show (convert value)
しかしこれは危険である.たとえば,以下のプログラムは型検査機をループさせられる.
code: ins15.hs
class D a
class F a b | a -> b
instance (D c, F a c) => D a -- 'c' is not mentioned in the head 同様に,カバレッジ条件を解除したくなるかもしれない.
code:ins16.hs
class Mul a b c | a b -> c where
(.*.) :: a -> b -> c
instance Mul Int Int Int where (.*.) = (*)
instance Mul Int Float Float where x .*. y = fromIntegral x * y
instance Mul a b c => Mul a b c where x .*. v = map (x.*.) v 3番目のインスタンス宣言は,カバレッジ条件に従わない.そしてたしかに(多少奇妙な)定義:
code:ins17.hs
f = \ b x y -> if b then x .*. y else y をすることで,インスタンス推論をループさせることができる.というのも制約(Mul a [b] b)が必要だからである.
UndecidableInstances拡張はまた,型族のインスタンスに課せられた制限の一部を取り除くために使用される.型シノニムインスタンスの決定可能性を参照せよ.
重複するインスタンス
OverlappingInstances
インスタンスの解決を確実に終了させることを目的とした,型検査を弱めるための非推奨の拡張.
IncoherentInstances
6.8.1以降
インスタンスの解決を確実に終了させることを目的とした,型検査を弱めるための非推奨の拡張
一般に,インスタンスの決定規則で説明したように,GHCは型クラス制約を解決するためにどのインスタンス宣言を使用すべきかが明確であることを要求している.GHCはインスタンスの解決を緩め複数のインスタンスがマッチすることを許可する方法も提供するが,最も具体的なインスタンスが存在することが必要である.さらに緩和することができ,最も具体的なインスタンスがあるかどうかに関係なく,複数のインスタンスがマッチすることを許可する方法も提供する.この節ではこの機能の詳細を説明する.
インスタンスの選択を制御するために,instanceキーワードの直後に書かれたプラグマを使用して,マッチするインスタンスが重複した際の,個々のインスタンスの決定方法を指定することができる.そのプラグマは, {-# OVERLAPPING #-},{-# OVERLAPPABLE #-},{-# OVERLAPS #-},{-# INCOHERENT #-}のいずれかである.
マッチング動作は,2つのモジュールレベルの言語拡張フラグ,OverlappingInstancesと IncoherentInstancesの影響も受ける.これらの拡張は,きめ細かなインスタンス毎のプラグマの方が良いという考えから(GHC 7.10から)非推奨となった.
より正確な仕様は以下の通りである.OverlappingであるのかIncoherentであるのかという情報は,インスタンス宣言自体が持つ特性であり,次のように制御される.
次の場合,インスタンスはincoherentである.
インスタンスにINCOHERENTプラグマがある.
インスタンスにプラグマがなく,IncoherentInstancesを使用してコンパイルされたモジュール内にある.
インスタンスは,次の場合にoverlappableである.
OVERLAPPABLEまたは OVERLAPSプラグマがある.
インスタンスにプラグマがなく,OverlappingInstancesを使用してコンパイルされたモジュール内にある.
インスタンスがincoherentである.
インスタンスは,次の場合にoverlappingである.
OVERLAPPINGまたは OVERLAPSプラグマがある.
インスタンスにプラグマがなく,OverlappingInstancesを使用してコンパイルされたモジュール内にある.
インスタンスがincoherentである.
ここで,あるクライアントモジュールで,ターゲット制約のインスタンス(C ty1 .. tyn)を検索しているとする.検索は次のように行われる.
ターゲット制約に一致するすべてのインスタンスIを見つける.つまり,対象の制約はIのインスタンスになにかを代入したものである.あくまでこれらのインスタンス宣言は候補である.
次の両方が成り立つ候補IXを削除する.
より具体的なもう1つの候補IYがある.つまり,IY はIXになにかを代入した形だが,その逆が成り立たない.
IXがoverlappable,IYがoverlappingのいずれかである(「かつ」ではなく,この「また」により,クライアントはライブラリを変更することなく,意図的にライブラリの提供するインスタンスを上書きすることができる).
incoherentでない候補が1つだけ残っている場合は,それを選択する.残りの候補がすべてincoherentである場合は,任意の候補を選択する.そうでなければ(すなわち,非incoherentな候補が複数生き残った場合は)検索は失敗する.
前のステップで選択した候補がincoherentであれば,検索は成功し,その候補が返される.
そうではない場合,対象の制約にマッチしないが, 単一化(unify)するinstanceを全て見つける.この,候補ではないinstanceは,対象の制約がより細かく具体化された場合にマッチする可能性がある.この時得られたインスタンスが全てincoherentであれば,検索は成功し,選ばれた候補を返す.そうでなければ検索には失敗する.
これらの規則は,インスタンスが使用される場所である,クライアントモジュールのフラグ設定の影響を受けないことに留意せよ.これらの規則により,ライブラリの作成者は,クライアントに知らせなくても,重複するインスタンスに依存するライブラリを設計することができる.
エラーは,(インスタンス自体が定義されるときに)積極的に報告されるのではなく,(制約を解決しようとしているときに)遅延して報告される.例えば,次を考えてみよう.
code:ins18.hs
instance C Int b where ..
instance C a Bool where ..
これらは重複する可能性があるが,フラグの設定に関係なく,GHCはインスタンス宣言自体について不平を言うことはない.後で制約(C Int Char)を解こうとする場合は,最初のインスタンスだけが一致し,全く問題ない.(C Bool Bool)も同様である.しかし,(C Int Bool)を解こうとすると,両方のインスタンスが一致するので,エラーになる.
ルールのより実質的な例として,次のような例を考える.
code: ins19.hs
instance {-# OVERLAPPABLE #-} context1 => C Int b where ... -- (A) instance {-# OVERLAPPABLE #-} context2 => C a Bool where ... -- (B) instance {-# OVERLAPPABLE #-} context3 => C a b where ... -- (C) instance {-# OVERLAPPING #-} context4 => C Int Int where ... -- (D) 型推論エンジンが制約C Int [Int]を解く必要があるとする.この制約の場合,インスタンス(A), (C), (D)と一致するが,最後のインスタンスがより具体的であるため,それが選択される.
もし(D)が存在しなかった場合,(A)と(C)はまだ一致するが,どちらも最も具体的ではない.その場合,IncoherentInstancesが有効になっていない限り,プログラムは拒否される.有効になっている場合は,受け入れられ,(A)または(C)が任意に選択される.
前者のインスタンス宣言の先頭が後者のインスタンスに何かを代入した形の場合,かつその場合に限り,前者インスタンス宣言は他のものよりもより具体的である,と定義する.例えば,「a:= Int」を代入することで(C)から(D)に進むことができるので,(C)よりも(D)の方がより具体的である.
GHCは重複するインスタンスの決定について保守的である.例えば:
code: ins20.hs
f x = ...
fの右辺から,制約C b [b]が得られるとする.しかし,この場合,GHCはインスタンス(C)を選択しない.なぜなら,特定のfの呼び出しでは ,bはIntに具体化される可能性があり,その際はインスタンス(D)のほうがより具体的になるからである.そのため,GHCはこのプログラムを拒否する.
ただし,(D)を含むモジュールをコンパイルするときにIncoherentInstances拡張を有効にすると,GHCは代わりに(C)を選択する.その後のインスタンス化の問題については文句を言わない.
fに型シグネチャを付けたので,GHCはfが指定された型を持つことをチェックしなければならなかったことに注意せよ.我々が型シグネチャを与えず,GHCに推論させる場合を考えよう.この場合,GHCは(前述と同様の理由で)制約C Int [b]を単純化することを控えるが,プログラムを拒否するのではなく,型を推論する.
code: ins21.hs
GHCに推論させることにより,どのインスタンスを選ぶかという疑問は,fを呼び出す側でbの型についてよく分かるまで遅らせることができる.
FlexibleContexts拡張を使えば,このような型シグネチャを自分で書くことができる.
インスタンス宣言自体でもまったく同じ状況が発生する可能性がある.
code: ins22.hs
class Foo a where
f :: a -> a
f x = ...
以前のように,制約C Int [b]はfの右辺から生じるとする.GHCはインスタンスを拒否し,以前のようにそれが制約C Int [b]を解決する方法を知らないと文句を言う.なぜなら,それは複数のインスタンス宣言にマッチするからである.解決策は,インスタンス宣言のコンテキストに制約を追加して,インスタンスの選択を延期することである.
code: ins23.hs
instance C Int b => Foo b where f x = ...
(これを行うにはFlexibleInstancesが必要である)
警告
重複するインスタンスは注意して使用する必要がある.IncoherentInstancesがなくても,それらは矛盾を引き起こす可能性がある(つまり,プログラム場所が変われば,異なるインスタンス選択が行われる).次の場合を考えよう.
code: ins24.hs
{-# LANGUAGE OverlappingInstances #-} module Help where
class MyShow a where
myshow :: a -> String
instance MyShow a => MyShow a where myshow xs = concatMap myshow xs
showHelp :: MyShow a => a -> String showHelp xs = myshow xs
{-# LANGUAGE FlexibleInstances, OverlappingInstances #-} module Main where
import Help
data T = MkT
instance MyShow T where
myshow x = "Used generic instance"
myshow xs = "Used more specific instance"
main = do { print (myshow MkT); print (showHelp MkT) } 関数showHelpでは,GHCには重複するインスタンスが見えないので,MyShow [a]インスタンスを文句なしに使用する.mainでmyshowを呼ぶとき,GHCはMainモジュールの重複するインスタン宣言を使ってMyShow [T]制約を解決する.その結果,プログラムは次のように出力される.
code: console
"Used more specific instance"
"Used generic instance"
(現在実装されていないが,このコードを実行した際の挙動として,後のインスタンス宣言がローカルのものと重複する可能性があるという理由でHelpモジュールを拒否するという挙動も可能であろう.)
インスタンス宣言内の型シグネチャ
InstanceSigs
7.6.1以降
インスタンス定義内のメソッドに型シグネチャを宣言できるようにする.
Haskell 2010では,インスタンス宣言に型シグネチャを書くことはできないが,書いた方が都合がよい場合は,InstanceSigs拡張を使えば書けるようになる.例えば,
code: ins25.hs
data T a = MkT a a
instance Eq a => Eq (T a) where
(==) :: T a -> T a -> Bool -- The signature
(==) (MkT x1 x2) (MkTy y1 y2) = x1==y1 && x2==y2
詳細
インスタンス宣言内の型シグネチャは,具体的な型によるインスタンスの具体化のために,クラス宣言で書かれたものと同じであるか,より多相的でなければならない.たとえば,これは問題ない.
code: ins26.hs
instance Eq a => Eq (T a) where
(==) :: forall b. b -> b -> Bool
(==) x y = True
ここでは,インスタンス宣言内の型シグネチャは,インスタンス化されたメソッドで必要とされるものよりも多相的である.
インスタンス宣言内のメソッドのコードは,予想どおり,インスタンス宣言内で指定された型シグネチャに対して型検査される.そのため,インスタンスの型シグネチャが必要以上に多相的であれば,コードもそうである必要がある.
型シグネチャを書きたいという要望の理由の1つは,単純なドキュメントである.もう1つの理由は,スコープ付きの型変数を使うためである.例えば,
code: ins27.hs
class C a where
instance C a => C (T a) where
foo :: forall b. b -> T a -> (T a, b) foo x (T y) = (T y, xs)
where
ScopedTypeVariables拡張 (字句的スコープを持つ型変数)も指定した場合,forall bはfooの定義,特にxsの型シグネチャにも適用される.
オーバーロードされた文字列リテラル
OverloadedStrings
6.8.1以降
オーバーロードされた文字列リテラルを有効にする(たとえば,IsStringクラスを介して脱糖される文字列リテラル )
GHCはオーバーロードされた文字列リテラルをサポートする.通常,文字列リテラルはString型だが,オーバーロードされた文字列リテラルを有効にすると(OverloadedStringsを使用すると),文字列リテラルの型は (IsString a) => aになる.
これにより,ByteString,TextといったStringのような型の他のバリエーションで通常の文字列の構文が使えるようになる.文字列リテラルは整数リテラルと非常によく似た動作をする.つまり,文字列リテラルは式とパターンの両方で使用できる.パターン内で使用された場合,文字列リテラルは整数リテラルと同じ方法で等価テストに置き換えられる.
IsStringクラスは次のように定義されている.
code:ins28.hs
class IsString a where
fromString :: String -> a
唯一の事前定義済みインスタンスは,文字列を通常どおりに機能させるための明白なインスタンスである.
code:ins29.hs
instance IsString Char where fromString cs = cs
IsStringクラスは,デフォルトではスコープ内にない.(例えば,それに対するインスタンス宣言を与えるために)IsStringクラスに明示的に言及したいのであれば,モジュールGHC.Extsからそれをインポートすることができる.
OverloadedStringsが有効になっている場合,Haskellのデフォルトのメカニズム(Haskell Language Report, Section 4.3.4)では文字列リテラルをカバーするように拡張される.具体的には:
default宣言内にあるそれぞれの型はNum か IsString のインスタンスである
default宣言が与えられていない場合,モジュールはdefault(Integer, Double, String)という宣言を含んでいるかのように振る舞う.
標準のデフォルトのルールは以下のように拡張される:すべての未解決の制約に標準クラスまたは IsStringが含まれ,少なくとも1つがNumクラスまたは IsStringクラスである場合,defaultingが適用される.
したがって,たとえば,式 length "foo"では,IsString a0の曖昧な使用が発生し,上記の規則のため,Stringにdefaultされる.
小さな例:
code:ins30.hs
module Main where
import GHC.Exts( IsString(..) )
newtype MyString = MyString String deriving (Eq, Show)
instance IsString MyString where
fromString = MyString
greet :: MyString -> MyString
greet "hello" = "world"
greet other = other
main = do
print $ greet "hello"
print $ greet "fool"
パターンマッチングは等式比較に変換されるので,Eqをderivingするのはパターンマッチングが機能するために必要である.
オーバーロードされたラベル
OverloadedLabels
8.0.1以降
#fooのようなOverloadedLabels構文を使えるようにする.
GHCはオーバーロードされたラベル,その解釈がその型とそのリテラルの両方に依存するかもしれない識別子の形式をサポートする.OverloadedLabels拡張が有効になっているとき,オーバーロードされたラベルは接頭辞にハッシュを使って#fooのように書くことができ,この式の型は実際はIsLabel "foo" a => aである.
IsLabelクラスは次のように定義されている.
code: ins31.hs
class IsLabel (x :: Symbol) a where
fromLabel :: a
これは,IsStringクラス(オーバーロードされた文字列リテラルを参照 せよ)にかなり似ているが,ラベルのテキストを型レベルの文字列として利用できるようにする追加の型パラメータがある(型レベルのリテラルを参照せよ)ことに注意せよ.GHC8.0ではfromLabelがProxy# xという余分な引数を持っているが,代わりに型適用(型適用の可視化を参照せよ)が使えるようになったため,GHC8.2ではこれは削除されている.
このクラスの定義済みインスタンスはない.デフォルトでは定義済のインスタンスは有効範囲にないが,GHC.OverloadedLabelsをインポートすることで有効範囲にすることができる.IsStringクラスと異なり,IsLabelクラスには特別なデフォルティングルールはない.
型検査中に,GHCは#fooのようなオーバーロードされたラベルの出現をfromLabel @"foo"に置き換える.これはなんらかの型alphaを持ち,型クラス制約IsLabel "foo" alphaの解決を必要とする.
この拡張の目的は,レコードのオーバーロードされたフィールドとおそらく匿名レコードをサポートするためにIsLabelを使うことである.したがって,将来,基本データ型(特に(->))のインスタンスが与えられる可能性がある.
RebindableSyntaxが有効になっている場合,オーバーロードされたラベルを脱糖するのに使われるのは必ずしもGHC.OverloadedLabels.fromLabelでなく,スコープに入っているなんらかのfromLabel関数である.
オーバーロードされたラベルを書くとき,ハッシュ記号とそれに続く識別子の間にスペースがあってはならない.MagicHash拡張は,後置のハッシュ記号を使用しているので,OverloadedLabelsと MagicHashが両方有効になっている場合,x#yはx# yの意味だが,OverloadedLabelsのみが有効になっている場合,それはx #yの意味である.UnboxedTuples拡張は(#を単一の語彙素にするため,UnboxedTuplesが有効になっているとき,左括弧とオーバーロードされたラベルの間にスペースを入れる必要がある.混乱を避けるために,OverloadedLabels拡張を使うときはハッシュの前にスペースを入れることを強く奨励する.
.hscファイル(「HaskellのCコードへのインターフェースを書く:hsc2hs」を参照せよ)でOverloadedLabels(あるいはハッシュ記号を利用する他の拡張機能)を使用する場合,ハッシュ記号がhsc2hsディレクティブとして扱われるのを防ぐために,ハッシュ記号を2回書く必要がある(#fooの代わりに,##fooとする必要がある).
以下に,オーバーロードラベルをレコードセレクタとして使用する方法を示す.これは型レベルリテラルのレコードアクセスの例の拡張である.
code:ins32.hs
{-# LANGUAGE DataKinds, KindSignatures, MultiParamTypeClasses,
FunctionalDependencies, FlexibleInstances,
OverloadedLabels, ScopedTypeVariables #-} import GHC.OverloadedLabels (IsLabel(..))
import GHC.TypeLits (Symbol)
data Label (l :: Symbol) = Get
class Has a l b | a l -> b where
from :: a -> Label l -> b
data Point = Point Int Int deriving Show
instance Has Point "x" Int where from (Point x _) _ = x
instance Has Point "y" Int where from (Point _ y) _ = y
instance Has a l b => IsLabel l (a -> b) where
fromLabel x = from x (Get :: Label l)
オーバーロードされたリスト
OverLoadedLists
7.8.1以降
オーバーロードされたリスト構文を使えるようにする(例:IsListクラスを介したリストの脱糖)
GHCはリスト記法のオーバーロードをサポートしている.リストを構成するための記法を要約してみよう.Haskellでは,リスト記法は次の7つ方法で使える.
code:ins33.hs
[] -- Empty list
OverloadedLists拡張が有効になっているとき,前述の7つの表記は次のように脱糖される.
code:ins34.hs
[] -- fromListN 0 []
x -- fromListN 1 (x : []) x,y,z -- fromListN 3 (x : y : z : []) x .. -- fromList (enumFrom x) x,y .. -- fromList (enumFromThen x y) x .. y -- fromList (enumFromTo x y) x,y .. z -- fromList (enumFromThenTo x y z) この拡張機能により,プログラマはリスト記法を使用して,Set,Map,IntMap, Vector,Text,Arrayといったデータ構造を構築できる.次のコードは,いくつかの例を示している.
code:ins35.hs
リストパターンもオーバーロードされている.OverloadedLists拡張が有効になっているとき,これらの定義は次のように脱糖される.
code:ins36.hs
f [] = ... -- f (toList -> []) = ...
(ここでは,変換にview pattern構文を使用している.view patternの項を参照せよ.)
IsListクラス
上記の脱糖衣では,toList,fromList, fromListNという関数はすべてIsListクラスのメソッドであり,IsListクラス自体はGHC.Extsモジュールからエクスポートされている.型クラスは次のように定義されている.
code:ins37.hs
class IsList l where
type Item l
fromListN :: Int -> Item l -> l fromListN _ = fromList
IsListのクラス及びそのメソッドはOverloadedLists拡張と組み合わせて使用されることが意図されている.
型関数Itemは,構造体lの要素の型を返す.
関数fromListは,与えられたItem lのリストから構造体lを作る.
関数fromListNは,入力リストの長さをヒントとして取る.その動作はfromListと同等であるべきだ.このヒントは,fromListと比較して,構造体lのより効率的な構築に使用できる.与えられたヒントが入力リストの長さと等しくない場合,fromListNの振る舞いは規定されない.
関数ToListメソッドは,fromListの逆でなければならない.
リスト記法をまったく新しいデータ型に役立てるためにIsListの新しいインスタンスを宣言するのはまったく問題ない.次はいくつかの例である.
code:ins38.hs
fromList = id
toList = id
instance (Ord a) => IsList (Set a) where
type Item (Set a) = a
fromList = Set.fromList
toList = Set.toList
instance (Ord k) => IsList (Map k v) where
type Item (Map k v) = (k,v)
fromList = Map.fromList
toList = Map.toList
instance IsList (IntMap v) where
type Item (IntMap v) = (Int,v)
fromList = IntMap.fromList
toList = IntMap.toList
instance IsList Text where
type Item Text = Char
fromList = Text.pack
toList = Text.unpack
instance IsList (Vector a) where
type Item (Vector a) = a
fromList = Vector.fromList
fromListN = Vector.fromListN
toList = Vector.toList
再バインド可能な構文
OverloadedListsを使ってリスト記法を脱糖衣するとき,GHCはモジュールGHC.ExtsのfromList(etc)メソッドを使う.これを実現するためにわざわざGHC.Extをインポートする必要はない.
ただし,RebindableSyntaxを使用すると,GHCは代わりにスコープ内にあるtoList,fromList,および fromListNという名前を持つものを,それがなんであれ,使用する.つまり,これらの関数は再バインド可能ということだ.c.f. 再バインド可能な構文および暗黙のPreludeインポート.
デフォルティング
現在,IsListクラスにはデフォルトの規則はない.実現可能ではあるが,例えば以下のようなデフォルト宣言の意味をどのように規定するかについてはあまり考察がなされていない.
code:ins39.hs
将来についての推測
OverloadedLists拡張の現在の実装は,リテラルだけが入っているリストを特別な方法で処理することで改善できる.より具体的には,コンパイラがそのようなリストをコンパクトな表現を使用して静的に割り当て,IsListインスタンスがそのコンパクトな表現を利用できるようにすることも可能かもしれない.この機能を搭載すれば,OverloadedLists拡張はOverloadedStrings拡張を包摂するのに良い地位を得るだろう(現在,特別な場合として,文字列リテラルは,静的に割り当てられたコンパクトな表現を利用している).
決定不能な(または再帰的な)親クラス
UndecidableSuperClasses
8.0.1以降
型検査器が終了しない可能性があるものも含め,すべての親クラス制約を許す.
UndecidableSuperClasses拡張を使用すると,親クラスではるかに柔軟な制約を使用できる.
型クラスは一般的に自分自身を親クラスとして持つことはできない.よって,以下は違法である.
code: ins40.hs
class C a => D a where ...
class D a => C a where ...
GHCは,型関数または型変数が関係しているとき,このテストを保守的に実行する.例えば,
code:ins41.hs
type family F a :: Constraint
class F a => C a where ...
GHCはこれについて文句を言うだろう.というのも,あとになって
code:ins42.hs
type instance F Int = C Int
を追加するかもしれず,そうすれば親クラスのループに入ってしまうからだ.型変数を使った例としては,のようなものがある.
code: ins43.hs
class f (C f) => C f
class c => Id c
C Idの親クラスを展開すると,最初にId (C Id)となり,その次に再びC Idとなる.
しかし,このような親クラスの制約は時々便利である.そして,保守的なチェックは実際の再帰が関係していないところでは面倒である.
さらに,真に再帰的な親クラスは時々便利である.次は実際の例だ(Trac#10318).
code:ins44.hs
class (Frac (Frac a) ~ Frac a,
Fractional (Frac a),
IntegralDomain (Frac a))
=> IntegralDomain a where
type Frac a :: Type
ここでは親クラスのサイクルはちゃんと終了するが,ちゃんと終了するということをコンパイラが理解するのはそれほど簡単ではない.
UndecidableSuperClasses拡張により,GHCは親クラス制約に対するすべての制約を取り除く.もし実際にループがあれば,GHCは有限の深さまでしか展開しない.