人気ブログランキング | 話題のタグを見る

箱入りデータ

Prelude の :info コマンドでリストの情報を見ると次のようになる。

Prelude> :info []
data [] a = [] | a : [a] -- Defined in ‘GHC.Types’
instance Eq a => Eq [a] -- Defined in ‘GHC.Classes’
instance Monad [] -- Defined in ‘GHC.Base’
instance Functor [] -- Defined in ‘GHC.Base’
instance Ord a => Ord [a] -- Defined in ‘GHC.Classes’
instance Read a => Read [a] -- Defined in ‘GHC.Read’
instance Show a => Show [a] -- Defined in ‘GHC.Show’
instance Applicative [] -- Defined in ‘GHC.Base’
instance Foldable [] -- Defined in ‘Data.Foldable’
instance Traversable [] -- Defined in ‘Data.Traversable’
instance Monoid [a] -- Defined in ‘GHC.Base’

冒頭に instance と表記されているところは、リストがどのタイプクラスに属しているかを示している。これを見ても、Eq, Monad, Functor, Ord, ... と数多くのタイプクラスのインスタンスであることがわかり面食らう。

タイプクラスとは耳慣れない用語だが、要するにそのタイプクラスに属する多相関数がリスト型のデータにも使えるということを示しているに過ぎない。例えば Show クラスの多相関数は show だがこれは色々なデータ型のデータを文字列にしてくれる関数だ。この関数は次のように 整数にも使えるし、リストにも使える。

Prelude> show 1
"1"
Prelude> show [1,2,3]
"[1,2,3]"

Show クラスの show 関数のようなわかりやすい関数は問題ないとして、問題は Monad, Functor, Applicative, Foldable, Traverseble, Monoid などのタイプクラスだ。

これらのタイプクラスは、それぞれ Monad であれば >>=, return などの関数、Functor であれば fmap などの関数がある。これらの関数がリスト型のデータに使えるということは分かるのだが。それらの関数でできる操作が被ってしまっているということだ。

例えばリストの要素を全て2倍にするという操作は、Functor クラスの fmap 関数を使うと次のように端的にかけるが、

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

これは、Monad でも書けてしまう。

Prelude> [1,2,3] >>= return . (*2)
[2,4,6]

また、Applicative でも、

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

Traversable でも書けてしまう。

Prelude> traverse (pure . (*2)) [1,2,3]
[2,4,6]

普通、ライブラリの関数は機能は重ならないものだ、Functor, Monad, Applicative, Traversable と4つものパッケージで同じことをするなんてどうかしている。

したがって、これは発想を変えなくてはならない。リストに適用したい加工の種類によって、タイプクラスがあるのではなく、リストをタイプクラスというツールボックスの道具を使って、なにをやりたいかを考えることだ。パッケージとデータの間のコペルニクス的転回だ。

実は、これらの4つのタイプクラスの関数は共通した特徴を持っている。それはリストというデータ型の箱を開けずにその中身を操作するという特徴だ。Prelude のコマンドライン上で (*2) 2 は計算できるが [1,2,3] は計算できない。それは [1,2,3] というリストのデータの中身である 1, 2, 3 がリストという箱の中に収められているからだ。

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

:73:1:
Non type-variable argument in the constraint: Num [t]

しかし、Functor クラスの関数 fmap を使うと、リストという箱を開けずに (*2) でその要素を2倍にできる。

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

つまり、Functor クラスが必要とされるのは、リストという箱を開けずにその中身をいじりたい時だ。これは Monad, Applicative, Traversable についても事情は同じだ。それなら、これらのタイプクラスを同じレベルで考えて、そのツールボックスに入っている関数を自由に選んで使えばいい。

これらのタイプクラスは、箱入りデータを加工するための道具の道具箱だと考えると、異なるパッケージで同じことをやってしまうという訳の分からなさが軽減する。

しかし、大切なのはそれだけではない。これらのタイプクラスの関数は多相関数だという大切な性質がある。つまり、色々なデータ型に適用できるということだ。Data.Tree モジュールの Tree 型は、リストと同じように Monad, Functor, Applicative, Traversable のインスタンスだ。したがって、リストに使った fmap などの関数がそのまま Tree 型にも使える。

Prelude Data.Tree> fmap (*2) (Node 1 [Node 2 [], Node 3[]])
Node {rootLabel = 2, subForest = [Node {rootLabel = 4, subForest = []},Node {rootLabel = 6, subForest = []}]}

そうであれば、嬉しいことに、Monad, Functor, Applicative, Traversable クラスの関数がどのように動作するのかをリスト型のデータで試しておけば、その類推で Tree 型にも使うことができることになる。

そこで、次回からは Traverse クラスの関数のテストを直接 Tree 型で行うのではなく、まずリスト型でやってみることにした。


by tnomura9 | 2016-10-17 06:25 | Haskell | Comments(0)
<< 自前のデータ型を Monad ... Applicative クラス >>