2008年12月21日日曜日

Haskell などで関数の型宣言がないときの混乱 (+_+)

Haskell では関数を定義するとき、その型を宣言しなくてもよい。ただし、後々のことを考え、ソースコードを読みやすくしておくために明示的に型宣言を書いておくことが勧められる。

 

Miranda と Haskell

なぜ関数プログラミングは重要か」では、説明が「Miranda に準じた」表記で書かれている。Miranda – Wikipedia によると、

研究目的ではない商用を目指した最初の純粋関数型言語であった。…

後発の Haskell は多くの面で Miranda に似ている。

Haskell と同様にというか、Haskell が影響を受けたと言うのが正確なのか知らないけれど 、 ^^;

Miranda は強い型付けをする言語だが、構文上は明確な型の宣言を必要としない。関数の型が明示されない場合、インタプリタは引数の型や引数の使われ方から型推論を行う。

(同上より)

 

引数が関数のとき、その関数はどのようなもの?

なぜ関数プログラミングは重要か」における説明では、関数の型が明示的に宣言されていない。そのため途中で頭が混乱してしまった。 (+_+) 具体的にどこでつまずいたのかということの前に、今更ながら当り前だけれど、関数の定義を読むときに最初に注目すべきところを確認しておく。

例えば、「与えた引数をそのまま返す」関数 hoge を定義する。

*Main> let hoge x = x
*Main> :t hoge
hoge :: t -> t

上記のように関数の型を調べると、hoge 関数を型 t に適用すると型 t が返される。

次に、hoge 関数に引数を一つ増やした hoge2 関数を定義する。hoge2 は、第1引数に関数 f 、第2引数にその関数 f を適用する対象を与えると、その結果を返すとする。

*Main> let hoge2 f x = f x
*Main> :t hoge2
hoge2 :: (t -> t1) -> t -> t1

関数の型を調べると、「型 t に対して適用すると 型 t1 を返す関数」を第1引数に与え、第2引数に型 t を与えると、型 t1 が返さえることが示される。

続けて同様に、hoge2 関数の引数を一つ増やした関数を定義する。hoge2 では引数として与える関数が x を 1 つ引数に取るだけだったが、今回は x, y の二つを引数として取るとする。

*Main> let hoge3 f x y = f x y
*Main> :t hoge3
hoge3 :: (t -> t1 -> t2) -> t -> t1 -> t2

関数の型を見れば、第1引数の関数の型が 2 つの引数を取ることがわかる。

 

関数適用の優先順位

さて、今は簡単な関数を定義する側だったので、関数の型を明示的に書かなくても関数の定義を見れば一目瞭然。引数の f は関数で、 x, y はその関数に適用する関数だと自分自身が一番わかっている。しかし、これが他人の書いた関数でもう少し複雑になり、部分適用やセクション、そして関数合成が定義に使われていると途端にわからなくなってしまう。(@_@;) 特にすぐに迷い道へと誘われる原因は、引数として与えられた関数。型宣言がないと、

「この引数は f と書かれているので多分関数なのだろうけれど、いったい引数をいくつ取るのだろうか? 部分適用されて使われているけれど、結局最終的にどういうこと?」

という風に。 ^^;

そういう関数の定義を見ると、どういう風に読めばいいのだろうかと思って悩んでしまう。(@_@;) しかし、基本に戻れば当り前のことだけれど、結合の優先度が高い「関数適用」に最初注目しなくてはいけない。例えば、A Gentle Introduction to Haskell: Functions には map 関数の定義が書かれており、

map                     :: (a->b) -> [a] -> [b]
map f  []               =  []
map f (x:xs)            =  f x : map f xs

これに対して次にような説明がされている。

関数適用はどんな中置演算子よりも高い優先順位をもっています。そのため、2 つめの等式の右辺は (f x) : (map f xs) のように構文解析されます。

(同上より、太字は引用者による)

そして、結合性に関して、

関数適用は、 e1e2 と書く。適用は左 結合性をもつので、(f x) y の括弧は省略することができ る

(Haskell 98 Report: 式 より, 太字は引用者による)

というを頭に入れておけば、先ほどの hoge3 関数を誰がどういう意図で書いたのかを知らずに見たとしても、

hoge3 f x y = f x y

の定義を見れば、f は関数で x, y はその引数であり、二つの引数を取ることがわかる。つまり、関数の引数がどのようなものであるかを知るには定義から推測するということ。てゆうか、なぜこんなにも当り前のことをちゃんと自分は認識していなかったのだろうかと反省する今日この頃… ^^;

 

つまづいたところ

ちなみに「なぜ関数プログラミングは重要か」でつまづいた箇所は、「3. 関数の貼り合わせ」における「リストのすべての要素を二倍にする関数」の説明で書かれている doubleandcons 関数の定義。前後の説明の流れはとりあえず無視して doubleandcons 関数だけを取り出して考えると、この関数は「リストのすべての要素を 2 倍にする関数」の中で「第1引数で与えられた要素を 2 倍して、第2引数で与えられたリストの先頭に追加する」役割を担っている。その部分だけ Haskell で書くと、以下のようになる。

doubleandcons = fandcons double
    where double n = 2 * n
          fandcons f el list = f el : list

この doubleandcons 関数は、次のように foldr の中で使われることによって、リストの全ての要素を 2 倍にする関数 doubleall が定義されている。

doubleall = foldr doubleandcons []

 

はじめこれを見たとき、何が何だかわからなかった。型宣言がされていなことと、部分適用がそれぞれの関数に使われていたので、最終的にどうなるのか途中でイメージできなくなってしまった。 (@_@;) 先ほど述べたように、型宣言がされていなくても定義を見ればその関数がどのようなものであるのかわかる。しかし、読んでいるときそのことになかなか気がつけず…。

今になるとなぜそんな誤解をしたのか呆れてしまうが、最初上の関数の定義を見たとき、doubleandcons の定義である fandcons double というのは、その下にある fandcons 関数の第2引数 el まで部分適用しているのかと思ってしまった。理解している人にとってみればなぜそんな勘違いをしたのか不思議に思うだろうけれど、double 関数が引数を一つ取るので、fandcons 関数に double を適用したとき、double の引数を fandcons 関数の第2引数に対応させて読めばいいのかな?と思ってしまったのだ。(+_+)

081221-001

fandcons の定義を見れば、

f el

と書かれていることより、f は関数で引数を一つとり、それが doubleandcons の定義の中で double に対応していて、doubleall 関数の定義では fandcons 関数の第1引数まで部分適用されているので、後は el と list の二つの引数を与えれば値が求まる関数であると素直に読めばよかったものを… ^^;

また、doubleall の定義における foldr の型は、

*Main> :t foldr
foldr :: (a -> b -> b) -> b -> [a] –> b

だから、第1引数は二つの引数をとる。よって、fandcons を使って doubleandcons を定義するのであれば、fandcons 関数の後ろ二つの引数のいずれかでも部分適用されていては型が合わないということは明白なのに…。

 

型宣言を書くなら…

結局、混乱した原因は、部分適用と型が明示的に宣言されていなかったことなので、どうしてもわからなくなかったら、そのまま無理して考えずに型宣言をして、なおかつ、部分適用の形をとらないで書いてから考えればよかったかも。 (+_+)

doubleall :: Num a => [a] -> [a]
doubleall xs = foldr doubleandcons [] xs

doubleandcons :: Num a => a -> [a] -> [a]
doubleandcons x xs = fandcons double x xs
    where double n = 2 * n
          fandcons f el list = f el : list