2013年12月23日月曜日

xmonadとHaskell(その14:型クラス)

(その13)では、XConfig l型のリファレンスで、layoutHookフィールドに定義すべき型が「l Window」型であることを確認し、この型として具体的にどんな型が使えるのかを調べるためにdefaultConfigのソースを覗いてFull a型、Tall a型などの型の値が使われていることを確認した。

しかし、自分でlayout設定をする時に、結局何を頼りにlayoutHookフィールドを設定すれば良いのか?XConfig l型のリファレンスを見ているだけでは、「l Window」型の型変数「l」が何を指しているのか分からずじまいだった。

今回はその答えに近づくために、xmonad関数の型シグニチャをいきなり見てみる。

xmonad :: (LayoutClass l Window, Read (l Window)) => XConfig l -> IO ()

この赤字の部分は、「型クラス制約」とよばれる。
というわけで、今回は「型クラス」の話だ。

型クラスって?


まずは、これ。
人間と猫がいて、挨拶をする関数を作りたいんだけれど、、

「hello」という関数名で人間にも猫にも挨拶しようとしたんだけれどうまくいかない。
普通、同じ関数名で複数の異なる処理を同時に定義することは出来ないのだ。

だけど、、、惜しいよね。
確かに「型」は違うけど、似たような処理じゃん??!

そこで登場するのが「型クラス」の仕組みなのだ。

これで、人にも猫にもhello出来るのだった。



すなわち、型クラスを使えば、同じような処理をするために、同じ関数名を使って、色んな型を扱えるのだ。ちなみに、こういうのを「多重定義」とかいうらしい。

だからもっと増やして、犬にだって、うさぎにだって、インコにだって、挨拶ができるようにすることができるし、更に実は、helloだけじゃなく、goodMornig関数やgoodNight関数等、複数の関数をこのグループに含めることも出来る。

というわけで、実際に上の型クラスを改良しながら、型クラスに親しんでみる。

型クラスを宣言してみる


まずは、4行目が型クラスを作る宣言の部分。

class Hello a where
classwhere」で型クラスを作りますよという書式だ。

そして、その中にある「Hello」が型クラスの名前。

型クラス名は、型や値コンストラクタと同様、大文字のアルファベットから始まる文字列だ。今度は、helloだけじゃなく、色々な挨拶をするので、「Aisatu」という名前の型クラスにしてみる。

更に、型クラスの名前に続いて、「a」と書かれている。
これが、この型クラスで定義する関数の共通の引数になる型を表現している型変数だ。

新しい型クラス「Aisatu」の宣言は以下のようになる。

class Aisatu a where

関数を登録


whereの後ろに、共通で使いたい関数を書いていくことになる。
この、型クラスに属する関数のことは、特に「メソッド」とか「メンバ関数」と呼ばれたりする。

5行目にそのhelloメソッドが書かれている。

hello :: a -> String

関数の型表現のみが宣言されている。
この中の型変数「a」が、型クラス名の後ろにある「a」と連動している。
つまりは、関数の引数として、人や猫等の色々なものにしたい部分を型変数「a」にして、関数の型宣言をすればよい。

ここで、別の挨拶関数も登録しておこう。

hello :: a -> String
goodMorning :: a -> String
goodNight :: a -> String

どの関数も、hello関数と同じで、ある型の値を一つ引数にとって文字列を返す関数だ。

型クラスの仲間であるインスタンスにする


型クラスの仕組みは、まず、あるグループを作って、そこでいろんな型で共通に使いたい関数を宣言する。次に、その関数を使いたい型は、それぞれ型についてのその関数を実装することで、仲間に入るという風になっている。

というわけで、まずは、ある型が仲間になりたいと宣言するための書式が7行目だ。

instance Hello Human where

型クラス宣言と似た感じで「instancewhere」で、ある型をある型クラスの仲間にしますよと宣言する書式だ。そして、仲間になる型を「インスタンス」と呼んだり、ある型を仲間にすることを「インスタンスにする」とか言ったりする。

次に「Hello」が仲間になる型クラスの名前で、その後ろの「Human」が仲間にする型の名前だ。

Humanを新しい型クラス「Aisatu」のインスタンスにするための宣言は、次のようになる。

instance Aisatu Human where

仲間になりたいと宣言したら、後は実際にその具体的な型で、メソッド関数が使えるようにプログラムすればよい。

whereの後ろ、8行目では、hello関数を実際に定義している。
すなわち、whereの後ろで、型クラスで宣言されているメソッドについて、Humanという具体的な型を引数とする時の具体的な処理をプログラムするのだ。

そのための仕組みであって、当たり前といえば当たり前だけれども、11行目を見ると、8行目と同じhello関数が実装している。まさに「多重定義」だ。

さて、新しいAisatu型クラスには、3つのメソッドがあるので、Humanをインスタンスにするには、この3つのメソッドを実装する必要がある。

instance Aisatu Human where
  hello h = "hello, " ++ h_name h
  goodMorning h = "goodMorning, " ++ h_name h
  goodNight h = "goodNight, " ++ h_name h

同様に、Neko型もAisatuのインスタンスにし、更には、新しくInu型を定義して、これもAisatuのインスタンスにしてみる。



さて、実際にghci上でこのコードを読み込んでみる。
そして、hello関数の型シグニチャを確認すると、

hello :: Aisatu a => a -> String

一番初めに見た「型クラス制約」というものが付いていることが分かる。

型クラス制約された関数


今までAisatu型クラスを作ってきた流れから察することができる通り、ここでの「型クラス制約」というのは、「helloの引数aは、Aisatu型クラスのインスタンスを使ってね」ということを表している。

すなわち、aというのは、多重定義を実装した具体的な型のことであり、その実装をしているからこそ、helloの引数に使えるのだ。逆に、実装をしていない型はhello関数の引数になりようがないだろう。

更に、型クラス制約というのは、hello関数そのものつまり、メソッドを使う場合だけにつくものではない。当たり前だが、内部でhello関数を使うような、関数を定義した時にも、その定義された関数に型クラス制約が付く。

例えば、

genkina_hello x = (hello x) ++ "!!!"

というhelloメソッドを内部で使うgenkina_hello関数の型シグニチャは

genkina_hello :: Aisatu a => a -> String

と表現される。

基本クラスと自動導出

おまけとして、巷のxmonad.hsの中でもちょくちょく見られる、便利な技の話。

haskellの標準ライブラリPreludeは聞いたことがあると思う。
ghciを起動するとカーソルに

Prelude>

とか出てくるアレだ。
そして、そのモジュールのリファレンスがこれだ。
http://hackage.haskell.org/package/base-4.6.0.1/docs/Prelude.html

色々と見たり聞いたりしたことのあるようなものが、並んでいると思う。

そんな中、「型クラス」についても標準ライブラリで見たことあるだろうものが基本クラスとして定められていたりする。

Eq型クラスの(==)や(/=)
Ord型クラスの(<)や(>)等

つまりは、これらの関数はメソッドであり、自作の型についても、これらを実装してインスタンスになれば、その関数を使うことができたりするのだ。

しかし、実は、これら基本型クラスのうち、次のものはについては、自作の型についてinstance〜where構文をつかった実装を必要とせずに、自動導出という機能を使って、一瞬でインスタンスになれたりするすごい機能がhaskellにはある。

自動導出できる型クラスはEq,Ord,Enum,Bounded,Show,Readだ。

使い方は、自作の型宣言の後ろに次のようにderivingキーワードとカッコで囲った型クラスを書くだけだ。

data Human = Human {h_name :: String} deriving(Eq,Ord,Show)

複数の型クラスのインスタンスにしたいなら、カンマで羅列すればいい。



0 件のコメント:

コメントを投稿