「deriving」機構への拡張

deriving節について推論される文脈

Haskell Reportは、正確にどんな場合にderiving節が有効かについてはっきりしない。例を示す。

  data T0 f a = MkT0 a         deriving( Eq )
  data T1 f a = MkT1 (f a)     deriving( Eq )
  data T2 f a = MkT2 (f (f a)) deriving( Eq )

自然生成されたEqのコードは以下のようなインスタンス宣言になるだろう。

  instance Eq a         => Eq (T0 f a) where ...
  instance Eq (f a)     => Eq (T1 f a) where ...
  instance Eq (f (f a)) => Eq (T2 f a) where ...

最初のものは明らかに問題ない。二番目も、それほど明らかではないが、問題ない。三番目はHaskell 98でなく、インスタンスの停止性を損なう危険がある。

GHCは保守的な立場を採っている。最初の二つは受け付けるが、三番目は受け付けない。規則はこうである。推論された文脈中の各制約は、型変数のみから成っていなければならず、型変数に重複があってもならない。

この規則はフラグに関係なく適用される。もっと風変りな文脈が必要なら、独立derivingの機構を使って自分で書くことができる。

独立deriving宣言

GHCは単独のderiving宣言を受け付けるようになった。これは-XStandaloneDerivingで有効になる。

  data Foo a = Bar a | Baz String

  deriving instance Eq a => Eq (Foo a)

構文は通常のインスタンス宣言と同じであるが、(a)derivingキーワードを使うことと、(b)where部がないことが異なる。以下の点に注意せよ。

  • 通常のインスタンス宣言でするのと全く同じように、文脈(この例では文脈は(Eq a))を明示的に指定しなければならない。(対照的に、データ型宣言に付属したderiving節においては文脈は推論される)。

  • deriving instanceの宣言は、形式と停止性に関して、通常のインスタンス宣言が従うのと同じ規則に従い、同じフラグで制御される。7.6.3. インスタンス宣言を見よ。

  • data宣言に付属するderiving宣言と異なり、インスタンスはデータ型よりも限定的になり得る(-XFlexibleInstancesを使うことを前提にすれば。7.6.3.2. インスタンス文脈に関する規則の緩和)。例として以下を考えよ。

      data Foo a = Bar a | Baz String
    
      deriving instance Eq a => Eq (Foo [a])
      deriving instance Eq a => Eq (Foo (Maybe a))
    

    これによって(Foo [a])(Foo (Maybe a))についての自動導出インスタンスが生成されるが、それ以外の(Foo (Int,Bool))といった型はEqのインスタンスにならない。

  • data宣言に付属したderiving宣言と異なり、GHCはデータ型の形式を制限しない。その代わり、単純に、GHCは指定されたクラスのための定型コードを生成し、それを型検査する。もし型エラーがあれば、それはあなたの問題である。(型エラーがあるなら、違反のあるコードをGHCが表示するだろう)。この方式の利点は、定型コードさえ型検査に通れば、GADTその他の妙なデータ型に対してインスタンスを自動導出できることである。例を挙げる。

      data T a where
         T1 :: T Int
         T2 :: T Bool
    
      deriving instance Show (T a)
    

    この例では、Tのデータ型宣言に付属して... deriving( Show )とは言えない。TはGADTだからである。しかし、独立derivingを使えば、このインスタンスを導出することができる

  • この独立deriving構文は、通常のderivingがnewtypeに対して一般化された(7.5.4. newtypeについての自動導出インスタンスの一般化)のと全く同じように一般化される。例えば次のように。

      newtype Foo a = MkFoo (State Int a)
    
      deriving instance MonadState Int Foo
    

    GHCは常に、インスタンスの最後のパラメタ(この例ではFoo)を導出対象の型として扱う。

より広範なクラスについてのderiving節(TypeableDataなど)

Haskell 98では、プログラマがデータ型宣言に「deriving( Eq, Ord )」と付け加えれば、これらのクラスの標準的なインスタンス宣言が生成される。Haskell 98では、deriving節に現れることのできるクラスは標準のEqOrdEnumIxBoundedReadShowだけである。

GHCはこれを拡張し、新たにいくつかのクラスが自動的に導出できるようにする。

  • -XDeriveDataTypeableを使うと、TypeableDataの両クラスのインスタンスを自動導出できる。これらのクラスは、ライブラリ中、それぞれData.TypeableData.Genericsで定義されている。

    Typeableのインスタンスは、データ型の型パラメタの数が七個以下で、その全てが類*を持っているときのみ自動導出できる。これは、TypeableクラスがScrap More Boilerplate: Reflection, Zips, and Generalised Castsで述べられている形を使って導出されているからである。(この論文の7.4節では、使われる複数のTypeableについて述べられているが、ライブラリが提供するのはTypeable1からTypeable7までだけである)。この条件を満たさない場合、プログラマは、そのデータ構築子に合った類を持つTypeableXを書き、そのデータ型についてのインスタンスを手で書くことができる。

  • -XDeriveFunctorを使うと、Functorクラスのインスタンスを自動導出できる。このクラスはGHC.Baseで定義されている。

  • -XDeriveFoldableを使うと、Foldableクラスのインスタンスを自動導出できる。このクラスはData.Foldableで定義されている。

  • -XDeriveTraversableを使うと、Traversableクラスのインスタンスを自動導出できる。このクラスはData.Traversableで定義されている。

どの場合でも、クラスをderiving節で言及する場合には、それがスコープになければならない。

newtypeについての自動導出インスタンスの一般化

newtypeを使って抽象型を定義するとき、元の型からインスタンスをいくつか継承させたいと思うことがあるかもしれない。Haskell 98では、EqOrdEnumBoundedに関しては自動導出を使うことでインスタンスを継承させられるが、それ以外の型については明示的なインスタンス宣言を書かなければならない。例えば、次のように定義したとする。

  newtype Dollars = Dollars Int 

そして、Dollarsに対して数値演算を使いたいなら、次のようにして明示的にNumのインスタンスを書かなければならない。

  instance Num Dollars where
    Dollars a + Dollars b = Dollars (a+b)
    ...

このインスタンスは、newtypeの構築子を適用したり排除したりしているだけである。特にくやしいのは、この構築子は実行時には存在しないので、このインスタンス宣言が定義する辞書はIntの辞書と全体を通して同じで、違いはこちらの方がより遅いことだけだ、という点である。

deriving節の一般化

このようなことをしなくても、GHCでは、-XGeneralizedNewtypeDerivingを使ってこの種のインスタンスを自動導出できるようになった。従って、次のように書くことができる。

  newtype Dollars = Dollars Int deriving (Eq,Show,Num)

そして、実装では、DollarsについてIntのものと同じNum用辞書が使われる。概念上は、コンパイラは次のような形のインスタンス宣言を導出している。

  instance Num Int => Num Dollars

このインスタンスは、型にしたがってnewtype構築子を付けたり外したりすることだけを行う。

構築子クラスのインスタンスも同様に自動導出することができる。例えば、次のように、状態と失敗を扱うモナド変換子を実装したとしよう。

  instance Monad m => Monad (State s m) 
  instance Monad m => Monad (Failure m)

Haskell 98では、次のようにしてパーサモナドを定義することができる。

  type Parser tok m a = State [tok] (Failure m) a

これは、上記のインスタンス宣言によって自動的にモナドになっている。拡張を使うと、Monadクラスのインスタンスを書くことなくパーサの型を抽象型にすることができる。次のようになる。

  newtype Parser tok m a = Parser (State [tok] (Failure m) a)
                         deriving Monad

この場合、導出されたインスタンス宣言は次の形になる。

  instance Monad (State [tok] (Failure m)) => Monad (Parser tok m) 

Monadは構築子クラスなので、このインスタンスはこのnewtypeの部分適用であり、左辺全体ではない。インスタンス宣言の文脈を生成するためにこの型宣言が「η変換」されたと考えることができる。

多引数型クラスについても、そのnewtypeがクラスの最後の引数であることを条件に、インスタンスを自動導出できる。この場合、deriving節に現れるのはクラスの「部分適用」形である。例として、次のようなクラスがあったとする。

  class StateMonad s m | m -> s where ... 
  instance Monad m => StateMonad s (State s m) where ... 

このとき、次のようにして、ParserについてのStateMonadのインスタンスを自動導出できる。

  newtype Parser tok m a = Parser (State [tok] (Failure m) a)
                         deriving (Monad, StateMonad [tok])

導出インスタンスは、部分適用されたクラスを最後にnewtypeに適用し、完全に適用された形にすることで得られる。次のようにである。

  instance StateMonad [tok] (State [tok] (Failure m)) =>
           StateMonad [tok] (Parser tok m)

この拡張の結果、newtype宣言での導出インスタンスは、ShowRead(これらは元の型とは異なる振る舞いをする)を除いた全てのクラスについて一様に扱われ(、元となった型の辞書を再利用することで実装され)る。

より精密な規定

自動導出によってインスタンス宣言が構築されるときの規則は以下である。次の宣言(型シノニムを展開した後のもの)を考える。

  newtype T v1...vn = T' (t vk+1...vn) deriving (c1...cm) 

ここで、

  • ciはクラスの部分適用であり、C t1'...tj'という形をとる。ただしCの引数の数はj+1である。すなわち、Cはただ一つの引数を欠いている。

  • kは、ci (T v1...vk)の類が正しくなるように選ばれる。

  • tは任意の型である。

  • 型変数vk+1...vnは、tにも、ciにも出現しない。

  • ciはどれも、ReadShowTypeableDataのいずれでもない。これらのクラスは型やその構築子を「素通りする」べきものではない。これらのクラスをnewtypeに自動導出することもできるが、それは通常の方法で行われ、この新しい機構は利用されない。

このとき、それぞれのciに対して、自動導出されるインスタンス宣言は以下のようになる。

  instance ci t => ci (T v1...vk)

うまくいかない例として、以下のものを考えてみよう。

  newtype NonMonad m s = NonMonad (State s m s) deriving Monad 

ここで、次のインスタンスを自動導出することはできない。

  instance Monad (State s m) => Monad (NonMonad m) 

なぜなら、型変数sState s mに現れており、「η変換」によって消し去ることができないからである。このderiving節が拒絶されるのは良いことである。NonMonad mは実際、同じ理由で、モナドではないからである。正しい型を持つ>>=を定義しようとしてみよ。できないはずである。

また、クラス引数の順序が重要になることに注意。これは、最後の引数についてしかインスタンスの自動導出ができないからである。もし上記のStateMonadが次のように定義されていたなら、上のようにParser型についてインスタンスを自動導出することは不可能だった。

  class StateMonad m s | m -> s where ... 

我々は、多引数型クラスには通常ひとつの「主要な」引数があり、それについてのインスタンス自動導出が最も重視される、という仮説を立てている。

最後に、これら全てはReadShowTypeableDataの各クラスには適用されない。これらには、組込みの導出規則(Haskellレポートの4.3.3節)が適用される。(EqOrdIxBoundedの各標準クラスについては、どちらの方法を使っても関係ない)