2009年5月18日月曜日

Haskell のモジュールの階層化と、型クラス - パラメータ多相とアドホック多相

0. 目次

  • 1. 同じ名前のフィールドラベルを持つ型を定義したい
  • 2. モジュールを分割し、階層化する
  • 3. 型の意味
  • 4. 型クラスの役割
  • 5. パラメータ多相と、アドホック多相
  • 6. 多相性とオブジェクト指向
  • 7. 演算子の意味
  • 8. 継承とジェネリクス
  • 9. Ad hoc の意味
  • 10. 型クラスの定義と、インスタンス化
  • 11. 余談: モジュールに分けない場合

 

1. 同じ名前のフィールドラベルを持つ型を定義したい

2 つの型が、類似している場合、フィールド名に同じ名前を使いたい。

例えば、「名前」と「年齢」を持つ、「犬」型を定義する。

このとき、フィールドラベルを使うなら、

data Dog = Dog {name :: String, age :: Int} deriving Show

「犬」型と同様のフィールドを持つ、「猫」型も定義。

data Cat = Cat {name :: String, age :: Int} deriving Show

しかし、同一モジュール (ここでは Main モジュール) で、同じフィールド名を持つ型を定義すると、エラーが発生する。 (+_+)

Multiple declarations of `Main.name'
...
Multiple declarations of `Main.age'
...

理由は、3.15 フィールドラベルをもつデータ型3.15.1 フィールドの選択 によると、、

フィールド名は選択子関数として使用する。変数として使用するときは、 フィールド名はオブジェクトからそのフィールドを取り出す関数として働く。 選択子はトップレベルの束縛なので局所変数によって覆い隠される。しかし、 同じ名前の他のトップレベルの束縛とは衝突することは出来ない。…

(太字は引用者による)

フィールド名は、単なる名前ではなく、フィールドの値を返す関数。名前空間は、トップレベルに所属するので、代数的データ型の中に書いているように見えても、バッティングに注意が必要ということ。

つまり、以下のように、フィールドラベルは「関数」となるので、同じ名前の関数を二つ作れない。

*Main> name $ Dog "pochi" 3
"pochi"

 

2. モジュールを分割し、階層化する

では、同じフィールド名を持つ、異なる型を定義したい場合、どうすればいいのだろう?

2.2.1. Modules vs. filenames によると、

How does GHC find the filename which contains module M? Answer: it looks for the file M.hs, or M.lhs.

GHC の場合、各々の型を別モジュールに定義し、別ファイルに含めるということ。

つまり、先ほどの例の場合、「犬」と「猫」型に対応したモジュールを作成し、それぞれのファイルに記述すれば良い。

説明に従い、最初に、「犬」型を ファイル Dog.hs に定義。

module Dog where
    data Dog = Dog {name :: String, age :: Int} deriving Show

ところで、GHC ではモジュールを階層化できる。

The Glorious Glasgow Haskell Compilation System User's Guide, Version 6.10.25.6.1. Haskell source files によると、

Usually, the file should be named after the module name, replacing dots in the module name by directory separators. For example, on a Unix system, the module A.B.C should be placed in the file A/B/C.hs, relative to some base directory.

モジュールのベースとなるディレクトリを想定し、そこからの相対位置で、モジュール名が決まる。モジュール名は、モジュールを配置したディレクトリの階層に対応させ、`.’ により階層化していることを示すということ。

例えば、先ほどの「猫」型をやめ、「三毛猫」型を Cat 階層に作りたい。この場合、

  1. Cat ディレクトリを作成し、
  2. 下記のモジュールを Mike.hs に記述し、Cat ディレクトリに配置。
  3. その際、モジュール名は、モジュールを配置したディレクトリに対応させるために Cat.Mike とする。
module Cat.Mike where
    data Mike = Mike {name :: String, age :: Int}

全体では、以下のようにファイルを配置する。

Dog.hs
Cat
-- Mike.hs

ただし、メインモジュールにおいて、フィールドラベルを用いて「名前」を表示したい場合、関数名 (フィールドラベル) をモジュール名で修飾しなくてはならない。

import Dog
import Cat.Mike
main = do print $ Dog.name $ Dog "pochi" 3
          print $ Cat.Mike.name $ Mike "tama" 2

できることなら、関数を適用するとき、モジュール名で修飾せず、シンプルに name と書きたい。

そのためには、「型クラス」を使い、関数のオーバーロードを行う必要がある。

 

3. 型の意味

その前に、「型」について整理しておく。

データベース実践講義」の「2.3 型とは」(p34) によると、

型とはいったい何か。基本的には、値の名前付き有限集合である。…

すべての型が、その型の値もしくは変数に作用する演算子の連想集合を持つ …

Data type - Wikipedia, the free encyclopedia には、

In a broad sense, a data type defines a set of values and the allowable operations on those values.

つまり、「型」とは

  1. 値の集合と、
  2. その値に対する、操作が定義されたもの。

上記より、オブジェクト指向における「クラス」を連想した。なぜなら、

  1. 「値」に相当するインスタンスと、
  2. 「操作」に相当するメソッドを持つため。

Data type - Wikipedia には、型 (データ型) と呼ばれるものには、いくつか種類があることが示されている。

上記データ型の違いの一例を挙げると、オブジェクト型では、内部状態を持つのに対して、Haskell のような代数的データ型では、値の集合を定義するのみで、操作を定義する場合、別に関数定義する。

 

4. 型クラスの役割

では、「型クラス」とは何か?

Type class - Wikipedia によると、

Type classes first appeared in the Haskell programming language, and were originally conceived as a way of implementing overloaded arithmetic and equality operators in a principled fashion.

 A Gentle Introduction to Haskell: Classes には、

There is one final feature of Haskell's type system that sets it apart from other programming languages. The kind of polymorphism that we have talked about so far is commonly called parametric polymorphism. There is another kind called ad hoc polymorphism, better known as overloading.

Here are some examples of ad hoc polymorphism:

  • The literals 1, 2, etc. are often used to represent both fixed and arbitrary precision integers.
  • Numeric operators such as + are often defined to work on many different kinds of numbers.
  • The equality operator (== in Haskell) usually works on numbers and many other (but not all) types.

(太字は引用者による)

型クラスは、Haskell の特徴の一つで、Haskell で初めて導入された。多相性には、パラメータ多相と、アドホック多相があり、後者はオーバーロードと呼ばれる。

型クラス(Type class - Wikipedia) の説明に戻る。

a type class is a type system construct that supports ad-hoc polymorphism. This is achieved by adding constraints to type variables in parametrically polymorphic types. Such a constraint typically involves a type class T and a type variable a, and means that a can only be instantiated to a type whose members support the overloaded operations associated with T.

090513-002.png上記について、「Haskell の代数的データ型と型クラス、instance 宣言の関係」で使い方を確認した。しかし、そのとき、代数的データ型と、型クラスの関係が理解しにくく、知識として定着しなかった。

オブジェクト指向におけるオーバーロード、オーバーライドには馴染みがある。しかし、パラメータ多相、アドホック多相、それに加えて、型クラスがどう絡んでいるのかイメージがしずらい。(+_+)

「アドホック多相は、関数を適用する対象を制約するための手段。 Haskell では、それを型クラスによって実現している。」

と頭に叩き込もうとした。しかし、直観的に理解できず、しっくり来なかった。 (@_@;)

090514-006.pngしかし、次のように考えたら、スッキリした。

  1. 型は値をグループ化する。
  2. 型クラスは、型をグループ化する。
  3. その結果、型クラスの制約が付いた関数は、その型クラスのグループに属していない型には適用できない。
  4. インスタンス化とは当該の型クラスに所属する宣言。

 

5. パラメータ多相と、アドホック多相

Type polymorphism - Wikipedia によると、Christopher Strachey が、二つの異なる多相について述べていたとのこと。

それによると、「アドホック多相」とは、

If the range of actual types that can be used is finite and the combinations must be specified individually prior to use …

それに対して、「パラメータ多相」とは、

If all code is written without mention of any specific type and thus can be used transparently with any number of new types …

つまり、アドホック多相は、関数を適用する型を制限するのに対して、パラメータ多相は、具体的な型について言及しないことにより、新しい型に対応できるようにするということ。

 

6. 多相性とオブジェクト指向

パラメータ多相と、アドホック多相は、オブジェクト指向において、どのように対応しているのだろうか?

Polymorphism (computer science) - Wikipedia によると、

In object-oriented programming, subtype polymorphism or inclusion polymorphism is a concept in type theory wherein a name may denote instances of many different classes as long as they are related by some common super class.[1] Inclusion polymorphism is generally supported through subtyping, i.e., objects of different types are entirely substitutable for objects of another type (their base type(s)) and thus can be handled via a common interface. Alternatively, inclusion polymorphism may be achieved through type coercion, also known as type casting.

オブジェクト指向においては、サブタイプ多相と呼ばれる。オブジェクトが、共通のインターフェイスを実装している場合、他のオブジェクトに置き換えることができるというもの。

また、Operator overloading - Wikipedia によると、

(less commonly known as operator ad-hoc polymorphism) …

operators like +, =, or == have different implementations depending on the types of their arguments.

演算子オーバーロードは、アドホック多相に相当するとのこと。

オーバーロードと言えば、以下のようにいくつか種類がある。

  • Function overloading, a software engineering process whereby multiple functions of different types are defined with the same name
  • Operator overloading, a software engineering process whereby operators such as + or - are treated as polymorphic functions having different behaviours depending on the types of arguments used
  • Method overloading a type of polymorphism where different functions with the same name are invoked based on the data types of the parameters passed

    (Overload - Wikipedia, the free encyclopedia より)

  •  

    7. 演算子の意味

    ところで、「演算子」というと、Java しか知らなかったとき、メソッドとの違いを明確にイメージしていた。

    `+’ のように、「メソッド名にできない記号が演算子」と言う意識。

    この Java の仕様は、以下で述べられている。

    Sun deliberately chooses not include operator overloading in the Java language.

    (Operator overloading - Wikipedia, the free encyclopedia より)

    Ruby は Java と違い、再定義可能な演算子 がある。

    |  ^  &  <=>  ==  ===  =~  >   >=  <   <=   <<  >>
    +  -  *  /    %   **   ~   +@  -@  []  []=  `

    Haskell は、 Haskell 98 字句構造 で述べられている。

    Python は、__XXXXX__() という形の 特殊メソッド を、クラスが実装することによって、同様のことが可能。

    そういえば、Haskell に触れるようになってから、関数と演算子の差異をあまり感じなくなった。なぜなら、関数の中置記法があるため。

    演算子 – Wikipedia とは、

    コンピュータプログラミングにおいては、主に記号を用いて演算を指示するものが演算子と呼ばれる。概ね数式などの記述を模倣しているが、一部の演算子に通常と異なる記号が用いられたり、副作用を持っていることがあるなど、数学の演算子とは異なる点もある。

    関数 f(x) の "f( )" も単項演算子であり、符牒となる文字列 "f" を関数子などと呼ぶ場合もある。関数子としては任意の文字列を使用することができ、代表的なものとして三角関数 "sin", "cos", "tan" などが挙げられる

    つまり、演算子も関数も、使える記号と記法が違うだけで、本質的な違いはない。

    考えてみれば、メソッドオーバーロードは、同一クラス内で、異なる引数に対する処理に、同じ名前を付けること。メソッドの引数を、関数プログラミングで言う適用する対象と見れば、メソッドオーバーロードは、メソッドが所属するクラスは同じでも、適用する対象が異なるという点で、演算子オーバーロードと似ている。

    「メソッドのシグニチャは、なぜ返り値を含まないんだ?」

    と、以前から疑問に思っていた。しかし、1 + 2 と 3 + 4 の結果、型が異なるような実装ができたらおかしいか。

     

    8. 継承とジェネリクス

    話を戻して、オブジェクト指向におけるアドホック多相とは、オーバーロードに相当する。

    先に挙げた Christopher Strachey  の言うところの

    「メソッドの引数の型がある範囲に限られている」

    ということによる。

    型が「限られている」という点から見ると、

    Ad-hoc polymorphism is generally supported through object inheritance, …

    (Type polymorphism - Wikipedia より)

    オブジェクト指向の継承も、アドホック多相に相当する。

    逆に、「限定されていない」と言うのは、090518-002.png

    In the object-oriented programming community, programming using parametric polymorphism is often called generic programming.

    ジェネリクスのこと。

     

    9. Ad hoc の意味

    ところで、「アドホック」というと

    アドホックな仮説 - Wikipedia

    という使われ方を連想する。言葉自体に、良いイメージがない。

    そもそもの意味は Yahoo!辞書 - ad hoc によると、

    ((限定))そのためだけに[の], 特別に[な]

    Ad hoc - Wikipedia には、

    Ad hoc is a Latin phrase which means "for this [purpose]". It generally signifies a solution designed for a specific problem or task, non-generalizable, and which cannot be adapted to other purposes.

     

    10. 型クラスの定義と、インスタンス化

    さて、最初の「犬・猫」型のコードに戻る。型クラスを使い、異なる型に、同じ関数名を適用できるようにしたい。

    そのためには、型をグループ化する、型クラスを定義する。

    Name.hs

    module Name where
        class Name a where
                getName :: a -> String

    次に Dog.hs

    module Dog where
        import Name
        data Dog = Dog {name :: String, age :: Int} deriving Show
        instance Name Dog where
               getName (Dog name age) = name

    同じようにして Cat/Mike.hs

    module Cat.Mike where
        import Name
        data Mike = Mike {name :: String, age :: Int}
        instance Name Mike where
                    getName (Mike name age) = name

    ついでに、メインモジュールにおいて、getName 関数を利用して

    「こんにちは!○○.」

    と出力する hello 関数も定義する。

    import Dog
    import Person
    import Name
    import Cat.Mike
    
    hello :: Name a => a -> String
    hello x = "Hello! " ++ getName x ++ "."
    
    main = do print $ getName (Mike "mike" 100)
              putStrLn $ hello (Mike "mike" 30)

     

    11. 余談: モジュールに分けない場合

    もし、モジュールに分けずにシンプルに書くとしたら、

    1. 犬と猫に共通の Pet 型を作り、
    2. そこで名前と年齢を持たせる。

    ついでに、犬と猫を同じ型にして、定義してみた。

    data Pet = Pet { name :: String, age :: Int }
    data MyPet = Dog Pet | Cat Pet
    
    getName :: MyPet -> String
    getName (Dog p) = name p
    getName (Cat p) = name p
    
    main = do print $ getName $ Dog (Pet "Pochi" 10)
              print $ getName $ Cat (Pet "Tama" 3)

     

    関連記事

    関連サイト