2008年6月29日日曜日

Haskell の関数合成

1. 関数合成の例

Haskell では (.) を用いると、2つの関数を合成できる。

例えば、与えられた数値に1加える関数 add1 と、与えられた数値を2倍する mul2 がある。

add1 x = x + 1
mul2 x = x * 2

100を2倍した後、1加えるには、

*Main> add1 $ mul2 100
201

100に1加えた後、2倍するには、

*Main> mul2 $ add1 100
202

上記の関数を(.) を使い、合成したい場合、

add1mul2 = add1 . mul2
mul2add1 = mul2. add1

これを使うには、

main = do print $ add1mul2 100   --- 201
          print $ mul2add1 100   --- 202

(.) を使うと、シンプルな関数を組み合わせて、複雑な関数を定義することができる。

 

2. 関数合成を理解するには、部分適用について先に知るのが良い

ふつうのHaskellプログラミング」で、はじめに戸惑ったのが、この

  • 「8.2 関数合成」 (p198)

の説明。

  • 「8.3 部分適用」 (p202)

の解説の前にあったので、すんなりと理解できなかった。

同書において、「関数合成」について、以下のように述べられている。

(.) 関数は、二つの関数を合成する関数です。「合成する」とは、「2つの関数を順番に適用する新しい関数を作る」ということです。(p198)

関数と関数を合成?

(.) を適用すると関数が返されるというのは、どういう意味だろう?(@_@;)

 

3. 関数合成 (.) も関数の一種

はじめて

.

を見たとき、この記号は、Haskell で「引数を省略して書くための、特別な表記」なのかと思った。

先ほどの例で言えば、(.) を使わずに add1mul2 関数を定義するなら、以下のように書ける。

add1mul2 x = add1 $ mul2 x

この定義と (.) を使った定義を比べると、

add1mul2 = add1 . mul2
add1mul2 の引数が消されてしまったかのように見える。

同書(p199) の説明に、関数 (.) の型が書かれてる。

(.) :: (b –> c) –> (a –> b) –> (a –>c)

つまり (.) は、

「引数として 2 つの関数を与えると、関数が返される関数である」

というように、関数に過ぎない。

ただし、「部分適用」 に関する知識がない状態で、この説明を理解することができなかった。

合成する前にあった、関数の引数が消えてしまっている。これはどういうことだろうか?

という疑問を持った。

 

4. 関数を合成する関数に対して「部分適用」

上記を理解するには、「部分適用」について把握しておく必要がある。

(.) の実装を見てみる。Haskell Code by HsColour によると、

(.)   :: (b -> c) -> (a -> b) -> a -> c
(.) f g x = f (g x)

関数の型宣言における、最後の

a –> c

が括弧で囲まれていない。なぜなら、関数の宣言は右結合であるため、括弧が必要ないから。

4.1.2 型の構文 によると、

関数の型t1 -> t2 という形式で、これは、型 (->) t1 t2 と同等である。関数の矢印は右結合である。

意味は全く同じだけれど、最後の括弧がない方が、(.) 関数を

「3 つの引数を与えると、値が返ってくる関数」

という意識で見ることができる。

部分適用について理解があれば、

(.) に対して、二つの関数を与えると、あと一つ引数を与えれば値が返される関数

となり、部分適用した状態であるということが分かる。

 

5. (.) 関数を使い、型を確認する

例えば、

  1. 適当に関数 f, g を定義し、
  2. 2つの関数を (.) 関数に部分適用し、
  3. 型を確認してみる。
Prelude> let f x = x + 10
Prelude> let g x = x * 2
Prelude> :t (.) f g
(.) f g :: (Num a) => a -> a

関数 f, g を (.) に部分適用した型は、

あと一つ引数として値を与えれば、値が返ってくる関数

であることが示される。つまり、

f . g

という式は、 (.) 関数に対して、引数を 2 つ与え、部分適用した状態である。

(.) 関数の定義に戻り、3 つ目の引数 x を見れば、

(.)   :: (b -> c) -> (a -> b) -> a -> c
(.) f g x = f (g x)

これが先ほど「消えてしまった」と感じた、関数の引数であることが分かる。

 

関連記事

1コメント:

p-nix さんのコメント...

過去のエントリへのコメント失礼します。『ふつうのHaskellプログラミング』 P199の解説に納得いかなくて、これって厳密には部分適用を使って関数を返しているのでは?と思いながら検索していたらこのページにたどり着きました。おかげさまで一歩前進できそうです。