2008年6月19日木曜日

Haskell における関数の型と、部分適用

1. 関数には型がある

ふつうのHaskellプログラミング」には、関数の型について、以下のように記述されている。

引数の型と返り値の型の組み合わせで関数の型を表現します。(p57)

関数に型なんてあるの (@_@?というのが最初の印象。

 

2. 関数の型を調べる

コマンドラインから ghci を起動。同書 (p202) に書かれていた、

「3 つの引数を渡されたら、その合計を返す関数」

を定義し、その型を調べてみる。

 

GHCi で関数を定義し、型の確認する

GHCi で関数を定義するには、

第2回 多相性(ポリモーフィズム)への理解を深める:ITpro によると、

GHCiの対話環境内で関数を定義する場合には, (...)  letの後にスペースを空けて入力する必要があります。

型を調べるには :type を使う。( :type:t と省略できる。)

 

「3 つの引数を合計する関数」を定義して、型を確認する

では、関数の定義から。

Prelude> let addThree a b c = a + b + c

addThree 関数の型を調べる。

Prelude> :type addThree
addThree :: (Num a) => a -> a -> a -> a

`a’ という文字が、関数の型に含まれている。

a は型変数(type variable) と言い、どんな型と置き換えてもよいことを表わすために用いられます。また、型変数を含む型のことを多相型 (polymorphic type) と呼びます。

(ふつうのHaskellプログラミング, p58)

よって、 addThree は引数に、型変数をとる多相型。

 

型クラスによる型の制約

(Num a) => の意味は、関数を適用できる型に、制約があることを表す。

型クラスを使うと、多相型に制約を付けれられます。このような制約の付いた多相性のことをアドホック多相(ad hoc polymorphism) と呼びます。 (...) 制約のない多相性のことはパラメータ多相(parameter polymorphism) と呼びます。

(同上, p237)

型変数 a が、Num クラスのインスタンスである必要がある。

 

3. 部分適用の意味

引数を一度にすべて渡さず、一部だけ渡しておくことを部分適用 (partial application) と言います。(同上, p202)

関数型言語を、これまで知らなかったので、「部分適用」は驚きの性質。(@_@;)

カリー化 - Wikipedia によると、

この技法は、Christopher Stracheyにより論理学者ハスケル・カリーに因んで名付けられたが、実際に考案したのはMoses Schönfinkelとゴットロープ・フレーゲである。

関数 ff:(X×Y)→Z の形のとき、f をカリー化したものを g とすると、gf:X→(YZ) の形を取る。非カリー化uncurrying)とは、これの逆の変換である。

カリー化とは直感的には「引数を幾つか固定すると、残った引数の関数が得られる」ということである。たとえば、除算の関数 div(x, y) = x / y をカリー化したものをcdivとし、inv=cdiv(1) とすると、inv は新しい関数となり、inv(y) = 1 / y 、つまり引数の逆数を返す関数になる。

部分適用は、クラスとインスタンスの関係を連想させる。メタ的な関数として、複数の引数を与える関数がある。そこから、部分適用により、具体的な関数を導出する、といった流れ。

 

定義した関数に対して部分適用

先ほどの addThree 関数に対して、部分適用をし、その型を調べてみる。

Prelude> :type addThree 1
addThree 1 :: (Num t) => t -> t -> t

Prelude> :type addThree 1 2
addThree 1 2 :: (Num t) => t -> t

Prelude> :type addThree 1 2 3
addThree 1 2 3 :: (Num t) => t

上記の結果を見ると、それぞれ

  • 「あと 2 つ引数を与えると、t 型の値が返ってくるよ。」
  • 「あと 1 つ引数を与えると、t 型の値が返ってくるよ。」
  • 「返ってきたのは t 型です!」

ということ。

 

部分適用した場合に決まる型

Maybe モナド (p263) の説明に出てきた、lookup 関数を調べてみる。

Prelude> :t lookup
lookup :: (Eq a) => a -> [(a, b)] -> Maybe b

Prelude> :t lookup "hoge"
lookup "hoge" :: [([Char], b)] -> Maybe b

Prelude> :t lookup "hoge" [("a",100),("b",200)]
lookup "hoge" [("a",100),("b",200)] :: (Num t) => Maybe t

第一引数を、文字列ではなくて数値にすると、

Prelude> :t lookup 1
lookup 1 :: (Num t) => [(t, b)] -> Maybe b

Prelude> :t lookup 1 [(1,100),(1,200)]
lookup 1 [(1,100),(1,200)] :: (Num t) => Maybe t

部分適用により、具体的な制約や型が付けられるのが分かる。

 

4. プログラムの中で型を調べる (GHCi を使わない場合)

Data.Typeable の typeOf を使うと、プログラムの中で型を調べることができる。

import Data.Typeable

addThree :: Int -> Int -> Int -> Int
addThree a b c = a + b + c

main = print $ typeOf $ addThree 1

追記(2008.6.20) : ファイルに保存した関数を、GHCi に読み込ませる場合には、 GHCi において

:l ファイル名

と書く。例えば、上記のコードを partial.hs として保存したとする。このfファイルを読み込み、型を調べるには、

Prelude> :l partial.hs
[1 of 1] Compiling Main             ( partial.hs, interpreted )
Ok, modules loaded: Main.
*Main> :t addThree
addThree :: Int -> Int -> Int -> Int
*Main> :t addThree 1 2
addThree 1 2 :: Int -> Int

関連記事