Haskell の関数合成について書いてある記事が意外と少なかったので、私が詰まづいたところをメモしておきます。
ここでは、部分適用の知識があることを前提にしたいと思います。
また、ここでは数学や圏論の話は極力抜きで。私自身が苦手なので。
ポイントフリースタイル
まず初めに、
f x = g x
という式があったとき、このx
を「ポイント」と呼びます。
そしてこの式は、
f = g
と書くことができます。
この書き方を「ポイントフリースタイル」と呼びます。
関数合成
複数の関数を結合したい場合、
f x = g $ h x
といった書き方をすると思います。
これはポイントがカッコ内にあるため、このままではポイントフリースタイルにすることはできません。
しかし、(.)
という関数を使うと上の式を、
f x = (g . h) x
と書くことができます。
この「二つの関数を繋げて一つの関数にする」ことを、「関数合成」と呼びます。
f = g . h
ポイントフリースタイルにすることができました。
二引数関数
Haskell には正確には二引数関数は存在しませんが、ここでは便宜上、二つの引数を取る関数をそう呼ぶことにします。
たとえば(++)
関数の型は、
(++) :: [a] -> [a] -> [a]
と、見た目上は二引数関数になっています。
では、例として二つの文字列を取り、さらに反転させる関数を考えてみましょう。
f x y = reverse $ (++) x y
単純に考えると、次のようになると思います。
-- f :: String -> String -> String
f = reverse . (++)
しかし、この式を実行すると
main.hs:1:19:
Couldn't match type `[a1] -> [a1]' with `[a0]'
Expected type: [a1] -> [a0]
Actual type: [a1] -> [a1] -> [a1]
In the second argument of `(.)', namely `(++)'
In the expression: reverse . (++)
In an equation for `f': f = reverse . (++)
というエラーが出てしまいます。
これは、要約すると「reverse . (++)
に引数を適用しても、返ってくるのが[a1] -> [a1]
という型(の関数)であるため、reverse
の引数の型[a0]
と一致しない」という内容です。
では、二引数関数を関数合成したい場合、どうすれば良いのでしょうか。
(.)
関数とは
そもそも(.)
関数とは何でしょうか。
型は次のように定義されています。
(.) :: (b -> c) -> (a -> b) -> a -> c
ここから推測すると、(.)
の正体は
- 第二引数
::a -> b
に第三引数:: a
を適用させる - 第一引数
::b -> c
に上の値:: b
を適用させる
関数だということになります。
ところで、先程出てきた
f = reverse . (++)
という式は、
f x y = ((.) reverse (++)) x y
と書くことができます。この式を変形させると、
f x y = ((.) reverse (++)) x y
f y = (reverse ((++) x)) y
となり、関数がカッコの中に入ってしまうためエラーが出ていました。
(.)
は、三番目の引数に二番目の引数を適用させる関数ですから、この時点で
f y = ((.) reverse ((++) x)) y
となっていれば良いのです。
つまり、式を成立させるには
f = ((.) (reverse)) . (++)
f = (reverse .) . (++) -- 上式と同じ
と書けば良いということになります。
応用
前述の関数を使って、いくつかのパターンを考えてみましょう。
三引数関数
f x y z = g $ h x y z
という三引数関数を、ポイントフリースタイルにしてみましょう。
f x y z = g (((h x) y) z)
f x y = (.) g ((h x) y)
f x = (.) ((.) g) (h x)
f = (.) ((.) ((.) g)) h
f = (((g .) .) . h -- 上式と同じ
n引数関数はパターン見えてきました。
引数あり . 引数あり
f x y = g x $ h y
という関数を考えてみます。
f x y = g x (h y)
f x y = flip g (h y) x
f x y = ((.) (flip g) h) y x
f = flip ((.) (flip g) h)
f = flip $ flip g . h -- 上式と同じ
flip
関数を併用すると、ポイントフリーにできる関数の幅も広がりそうです。
とりあえず今回はここまで。