Yet Another Haskell Tutorial 20

8.5 Instances

このセクションでは、複雑な instance 宣言のやり方が説明してある。何が複雑なのかというと、instance 宣言では、データ型をタイプクラスのインスタンスに宣言すると同時に、タイプクラスの関数の実装を、その型についてプログラムしなくてはならないが、その実装のプログラムが複雑な場合のことだ。

YAHT では、タイプクラスの実装が複雑になる例として、Functor (タイプ)クラスを挙げてある。Functor とは圏論の用語で関手のことだが、関手とは何かということが分からなくても、例文のプログラムは理解できる。

ちなみに、関手とは圏から圏への写像のことだ。圏とは何かといいうと、対象と射からなる構造のことだ。たとえば、[1,2]、[2,4] という2つのリストが対象であるとき、関数 f = (*2) は対象 [1,2] から [2,4] への射だ。なぜなら、集合 [1,2] の要素 1 に対し f 1 -> 2 だし、要素 2 に対し f 2 -> 4 だから、集合 [1,2] は関数(射) f によって [2,4] に写しだす事ができるからだ。このような対象 [1,2] と [2,4] および射 f の組みはひとつの構造を表しているといえる。この構造を表す対象と射の組みを圏という。(厳密には圏になるためには射の満たすべき条件があるのだがイメージを掴むためにはこれで十分だ。)

Prelude> let f = (*2)
Prelude> f 1
2

関数 f が [1,2] と [2,4] の射になっているのを確かめるには、f 1 -> 2, f 2 -> 4 という対応を全て調べる必要があるが、map 関数を使うと次のように一度に確かめる事ができる。

Prelude> map f [1,2]
[2,4]

ところで、[1,2], [2,4], f の組みは圏を表していたが、[3,6], [6,12], g = (*2) の組みも圏である。

Prelude> let g = (*2)
Prelude> map g [3,6]
[6,12]

この圏同士を関係づける射 F があると便利だ。つまり F [1,2] -> [3,6]、F [2,4] -> [6,12]、F f -> g となるような写像があれば圏 [1,2], [2,4], f を 圏 [3,6], [6,12], g に写すことができる。

例えば h = (*3) は [1,2] -> [3,6], [2,4] -> [6,12] の射になることができる。

Prelude> let h = (*3)
Prelude> map h [1,2]
[3,6]
Prelude> map h [2,4]
[6,12]

これらの関係を整理すると次のようになる。

[1,2] --(f)--> [2,4]
[3,6] --(g)--> [6,12]
[1,2] --(h)--> [3,6]
[2,4] --(h)--> [6,12]
f --(id)--> g

これからすぐ推測できるのは [1,2] --(g . h)--> [6,12] ということだ。これは次のように確かめることができる。

Prelude> map (g . h) [1,2]
[6,12]

このときの h は対象に関する関手で id :: f -> g が射に関する関手だ。対象の関手と射の関手をまとめて関手 (functor) と呼ぶ。

あまり理論的な内容に深入りすると迷子になるので、Functor クラスを見てみよう。Fuctor クラスの関数は fmap だ。

class Functor f where
  fmap :: (a -> b) -> f a -> f b

となっている。(a -> b) は同じ圏の対象の間の関数(射)だ。f は対象に関する関手だ。これを上の例に当てはめてみると、 (f :: [1,2] -> [2,4]) -> (h [1,2]) -> (h [2,4]) となる。とにかく試してみよう。

Prelude> fmap f (map h [1,2])
[6,12]
Prelude> map h [2,4]
[6,12]

このように、fmap 関数は圏 [1,2], [2,4], f と関手 h を使って [1,2] -> [6,12] を計算していることになる。つまり、

[1,2] --(h)--> [3,6] --(g)--> [6,12]

を計算しているのだ。ただし、g :: [3,6] -> [6,12] ではなくて、f :: [1,2] -> [2,4] を使っている。つまり、g = F f (上の例の場合は F = id) を利用している。わかりにくい場合は上の例を自分で入力していろいろ実験したり、対象と射と関手の関係を図示してみたりしてみるとなんとなく仕掛けが見えてくる。

これでやっと YAHT の Functor クラスのインスタンス宣言の例に取り組める。

その前にまず Fanctor タイプクラスの関数 fmap の意味について考えてみよう。Functor (タイプ)クラスの class 宣言文を再掲する。

class Functor f where
  fmap :: (a -> b) -> f a -> f b

上の宣言の f はタイプコンストラクタを表している。従って、fmap :: (a -> b) -> f a -> f b は fmap の第1引数が (a -> b) 型の関数、第2引数はパラメータの型が a 型の f 型のデータ、戻り値がパラメータの値が b 型の f 型のデータになる。f a を関数 f を値 a に関数適用した値と読み間違えると訳が分からなくなる。

実は、リスト型は Functor (タイプ)クラスのインスタンスだ。従って、リスト型にも fmap 関数があり、fmap :: (a -> b) -> [a] -> [b] である。要するに次のような fmap はきちんと作動する。

Prelude> fmap (*2) [1,2,3]
[2,4,6]

従って、fmap はリスト型以外のデータ型に使える map 関数だと考える事ができる。

そこで、fmap を関数適用できるリスト型以外のデータ型を考えてみよう。自前の2分木のデータ型 BinTree を次のように宣言する。

Prelude> :set +m
Prelude> data BinTree a = Leaf a | Branch (BinTree a) (BinTree a) deriving Show

この宣言を使ってデータ example を作ると次のようになる。

Prelude> let example = Branch (Leaf 1) (Branch (Leaf 2) (Leaf 3))
Prelude|

この example に fmap を関数適用するためには、BinTree 型を Functor (タイプ)クラスのインスタンスにしなくてはならない。

Prelude> instance Functor BinTree where
Prelude|   fmap f (Leaf a) = Leaf (f a)
Prelude|   fmap f (Branch left right) =
Prelude|     Branch (fmap f left) (fmap f right)
Prelude|

これで準備が整ったので、fmap を example に関数適用してみる。

Prelude> fmap (*2) example
Branch (Leaf 2) (Branch (Leaf 4) (Leaf 6))

リスト型の map 関数と同じ事が fmap によって2分木型に適用できることが分かる。

YAHT の2番目の例は Bintree 型を Eq (タイプ)クラスに宣言する事で、foo == bar のような等値演算子を使えるようにする事だ。これは、deriving (Show, Eq) のように deriving キーワードで簡単にできるが自前の instance 宣言をするときの注意点が書いてある。自前の instance 宣言は次のようになる。

instance Eq a => Eq (BinTree a) where
  Leaf a == Leaf b = a == b
   Branch l r == Branch l' r' = l == l' && r == r'
   _ == _ = False

Functor (タイプ)クラスへの BinTree のインスタンス宣言と同じように、再帰的データ型の場合の多層関数の定義のモデルだ。ただ、instance キーワードの次の Eq => a は、BinTree a 型のパラメータのタイプ変数 a の型が Eq (タイプ)クラスのインスタンスであるという制限をつけている。なぜなら、== の定義の際に a == b のように、BinTree 型のパラメータに == 演算子が使われているからだ。Bintree 型のパラメータが Eq (タイプ)クラスのインスタンスでなければ、== 演算子を使えない。

このようにインスタンス宣言のときに、Eq a => a のようにパラメータの型を制限する事を "context" (コンテキスト) と呼ばれている。Context とは文脈とか環境とか言う意味だ。

このコンテキストが使われた宣言では、インスタンス宣言が循環してしまう事態も起きる。YAHT の3番目の例はそのような場合を示している。

Eq => a コンテキストのもとで a 型のデータを MyEq のインスタンスに宣言しているが、

class MyEq a where
  myeq :: a -> a -> Bool

instance Eq a => MyEq a where
  myeq = (==)

次のように、MyEq => a のコンテキストのもとで、a 型のデータを Eq 型に登録してしまうと、

instance MyEq a => Eq a where
(==) = myeq

== と myeq の間で定義の循環が起きてしまい、エラーになってしまう。これは、closed-world assumption 問題として知られている。コンテキストを使ったインスタンス宣言では、上の例の2番目の定義をしてはいけない事になっているが、それをくぐり抜ける例が4番目の例だ、しかし、よくわからなかったのでスキップする。

8.6 Kinds

この節ではタイプコンストラクタの部分適用について書かれているようだが、よくわからなかったのでスキップする。

Yet Another Haskell Tutorial 21 へ続く ...
[PR]
by tnomura9 | 2013-03-02 03:05 | Haskell | Comments(0)
<< Yet Another Has... Yet Another Has... >>