2008年7月9日水曜日

Haskell の代数的データ型と型クラス、instance 宣言の関係

1. クラスとメソッドが主役で、「関数」は脇役だと思っていた

関数型言語である Haskell で使用される用語には、Java, Ruby などのオブジェクト指向な言語で使われている用語と、類似しているものがある。しかし、似て非なるものがあるので、気をつけなくてはいけない。 (+_+)

ところで、関数型言語に触る前は、

「関数」

というものを、一段低い存在として認識していた。

言語を学習した順序が C → Java → Ruby。途中で Smalltalk をちらっと横目で見た、という流れだったので、

「やっぱプログラムの中心はクラス、オブジェクトでしょ」

と思い込んでいた。

しかし、Ruby のブロック付きメソッドを見てから、

「ん?なんだこれ?」

と違和感と疑問を感じはじめ、 JavaScript の関数で

「関数もオブジェクトなの?」

と混乱し、そして Haskell で

という言葉に出会い、「関数」を再度見直し中。

これまで、関数(メソッド)は、オブジェクトの一機能。問題領域はクラス構造で捉え、メソッドをクラス間の関係の中で、どこへ配置するのがいいのか?を考えることがプログラミングだ、という視点が強固に刷り込まれていた。

 

2. Haskell の用語を、Java のアナロジーで考える

オブジェクト指向で考える場合、

  1. オブジェクトのひな形となるクラスを考え、メソッドを定義し、
  2. 操作のインターフェイスを抽出しつつ、
  3. クラス間の関係を作っていく。

Haskell でも、

  1. 自由に型を定義し、特定の型に適用する関数を定義し、
  2. 型によって振る舞いが異なる関数を定義したり、関数に適用する型を制約しながら、
  3. 関数を組み合わせていく。

「振る舞いが異なる」というのはオブジェクト指向的でよく使われる言葉。関数型ではそういう言い方はしないのかな?新しいことを理解しようとするとき、既にある知識をアナロジーとして考えるので、用語がごっちゃになってしまう。 ^^;

 

Haskell の代数的データ型、型クラス、インスタンス宣言

Java のアナロジーとして理解するならば、Haskell の

  • 代数的データ型」は、クラス間の構造的側面を表現したもの。
  • 型クラス」は、インターフェイス、もしくはデフォルトの実装が定義ができるのので、抽象クラスに相当するかな。
  • インスタンス宣言」は、型が「ある型クラスの制約が満たされていること」を表現するので、型クラスの実装という感じ。

「インスタンス」という言葉は、オブジェクト指向での使われ方と違っているので、はじめは混乱した。 (+_+) 「型クラス」においてデフォルトの実装ができること、型が複数の型クラスのインスタンスとなれることから、 Ruby におけるモジュールの mix-in に近いと考えればいいのだろうか?

ともあれ、異なるものをアナロジーを利用して理解していくと、その両者の類似と相違により、徐々に頭の中が混沌としてくる。

 

Haskell の基本的な用語を整理する

Haskell の基本的な用語を整理してみる。

  1. まず「関数」あり。
  2. 関数の引数として、具体的な型を指定したり、「型変数」を指定できる。型変数とはどんな型でも受け入れるもの。
  3. 関数の引数を制約する方法には 2 つある。
    1. 引数で型を指定する。
    2. 引数である型変数に、「型クラス」による制約を付ける。
  4. 型クラスは class 宣言によって定義し、「クラスメソッドの型宣言」と「デフォルトの実装」を持つ。
  5. 自分で型を定義したものを「代数的データ型」と言い、data 宣言によって行う。
  6. 型が、ある型クラスの制約を満たす場合、その型クラスのインスタンスであると言う。これは、 instance 宣言によって行なう。

 

3. 関数、型クラス、instance 宣言のイメージ

どんな型の値にも適用できる関数のイメージ

上記を、自分なりに具体的にイメージしてみた。

  1. 値の集合に対する名前である「」がある。
  2. 型には Int , Char などいろいな種類がある。

例えば、

「型変数 a を引数に取る関数」 f

  を想定する。この関数は、どんな型であっても適用することができるとする。適用する対象を制約しない。

080709-001

 

適用できる型を制限する関数のイメージ

次に、関数 f に適用する型を制限したいとする。

誰にでも愛想がよく、どんな相手とでも無差別にやりまくる関数はゴメンだという場合。このとき、「型クラス」使う。これは、関数 f の入口に特定の形態に一致したものだけが通過できる門のようなものを設置するというイメージ。または、門に鍵がついており、その鍵を持っている型だけが通過できるというところかな。

関数の内部では、それぞれの型が持ってきた鍵を利用して処理を進める。

型クラスは、二つの部分からなる。

  1. 型クラスのインスタンスになるために必要な契約を表わすクラスメソッドの型宣言。これに合うように、型クラスのインスタンスは、メソッドを実装しなくてはいけない。
  2. デフォルトの実装。これは、インスタンスがクラスメソッドの型宣言で、指定した契約を独自に実装しなかったときに使われる。ちなみに、この「型ごとにクラスメソッドを独自に実装すること」を多重定義と呼ぶ。

例えば、

型クラス Hoge を定義し、この Hoge クラスのインスタンスのみ適用する関数 f を定義したい

とする。関数 f を適用したい型は、

  • instance宣言をし、Hoge クラスのクラスメソッドを実装する。
  • または、Hoge クラスのデフォルトの実装に任せる。

関数 f  の方では、型変数に Hoge クラスのインスタンスのみ受け入れることを

(Hoge a) =>

によって明示する。

080709-002

 

4. 簡単な例

特定の型クラスの値にのみ適用できる関数の例

関数 f を定義し、引数である型変数 a が Hoge クラスのインスタンスのみ適用されるようにする。

ここでは、簡単な例とするために、関数 f の返す型を String とした。

class Hoge a 
instance Hoge Int
instance Hoge Char

f :: (Hoge a) => a -> String
f _ = "f"

main = do putStrLn $ f (100::Int)
          putStrLn $ f 'a'

 

デフォルトの実装と、多重定義の例

次に、関数 f を適用する型に応じて、振る舞いを変化させる。

  1. Hoge クラスにクラスメソッド hogef の型宣言と、デフォルトの実装を定義する。
  2. Hoge クラス のインスタンスでは、インスタンス宣言に従い hogef 関数を定義する。
  3. 関数 f の中でそれぞれの型に応じた関数 hogef を適用する。

class Hoge a where
    hogef :: a -> String
    hogef x = "Hoge default"
instance Hoge Int where
    hogef _ = "Hoge Int"
instance Hoge Char where
    hogef _ = "Hoge Char"

f :: (Hoge a) => a -> String
f x = "f : " ++ hogef x

main = do putStrLn $ f (100::Int)
          putStrLn $ f 'a'

 

参考記事

1コメント:

ZNATZ さんのコメント...

いろいろすっきりしました!ありがとうございました!

デフォルトの実装だけ分かりません。
hogef x = "Hoge default"

IntとCharの場合すべで実装されてますから、
デフォルトを呼び出す可能性はないと思いますが・・・