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

関連記事

1コメント:

m-matsum さんのコメント...

本文に引用されている「カリー化 - Wikipedia」の内容は、実際には間違っています。

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

ここの部分が間違いです。「引数を幾つか固定すると、残った引数の関数が得られる」は正しくはカリー化ではなく部分適用に関する説明です。

なお、今はもうリンク先のWikipediaの記事内容が修正されており、

>部分適用とは、複数引数ある関数の引数の一部だけに実引数を適用する操作のことで、上述の div(x, y) の例で言えば、div から inv を導出する操作を指す。
>一方、カリー化は cdiv を導出する操作であり、引数への値の適用までは行わない。

と補足されています。