Haskellの関数合成でひっかかってたこと

関数合成てのは

f(g(x))

こんなやつです。でもたまには次のようにやりたくなるわけですね。

h(x,y) = f(g(x,y))


Haskellでの関数合成は(.)です。これで上記をやろうとします。

-- 1引数関数
f x = x + 2
-- 2引数関数
g x y = x * y

-- fとgを合成しようとする
h = f . g
-- hに2引数渡そうとする
main = print $ h 3 5 -- => 17?

これは型エラーがでます。

    No instance for (Num (a0 -> a0))
      arising from a use of `f'    
      Possible fix: add an instance declaration for (Num (a0 -> a0))
      In the first argument of `(.)', namely `f'
      In the expression: f . g
      In an equation for `h': h = f . g

(a0 -> a0)だと?
...なんだこれ。


このエラーが出る度、仕方なく(.)の代わりに渋々($)で書き直していたりしていたわけですが、先日漸く理由がわかりました。
gは2引数関数として定義したつもりだけど、Haskellでは関数は全てCurry化されているため、そんなもの(2引数関数)はそもそも存在しないから。
そういえばそうでした。


つまりfと合成したgは、

f x = x + 2
g x = \y -> x * y
h = f . g -- \x -> ((\y -> x * y) + 2) -- 型エラー!

こうやって1引数関数と解釈され、合成されていたんですね。実際には演算子(+),(*)の使用上、合成出来ずに上記型エラーが出ているわけですが。


初めの数式の通りに2引数関数と1引数関数を合成したいときは...一回uncurryでもしますか。

h = curry $ f . uncurry g
main = print $ h 3 5 -- => 17

こうするとhは意図通り2引数関数(面倒だからそう呼ぶ)になります。

追記

@maoeさんと@cutseaさんに、もっと簡潔に書けると教えて頂きました。

h = (f.).g

型を調べてみます。

ghci> :type (f.)
(f.) :: Num c => (a -> c) -> a -> c
ghci> :type (f.).g
(f.).g :: Num c => c -> c -> c

確かに2引数になっています。
更に多い引数を合成する場合は、(.)のセクションを増やせば良いようですね。

-- 1引数
f x = x + 2
-- 2引数
g x y = x * y
-- 3引数
g' x y z = x * y * z
-- 4引数
g'' x y z w = x * y * z * w

-- 3引数と1引数の合成
h' = ((f.).).g'
-- 4引数と1引数の合成
h'' = ((f.).).).g''

まあそこまですることはあまりないかもですが。

追記2: Arrowへの導入?

この関数合成の流れでArrowへの導入とすると綺麗でしょうか。僕Arrow良く分かってないですが。
返り値を2つ返す関数と、2引数関数の合成です。

import Control.Arrow
-- 2引数
f x y = x - y
-- 1引数で返り値2つ(Tupleで返す)
g x = (x * 2 , x + 2)
-- gの左側
gl x = x * 2
-- gの右側
gr x = x + 2

-- 返り値を2つ返す関数(g)と2引数関数(f)の合成
h  = uncurry f <<< g
-- hと同じ
h' = uncurry f <<< gl &&& gr

main = do
  print $ h 4  -- (4 * 2) - (4 + 2)  => 2
  print $ h' 4 -- (4 * 2) - (4 + 2)  => 2

バグが入らない小さな関数を組み合わせてプログラミング。

高階関数でひっかかってたこと

関数合成関係ないですがついでに。高階関数つながりで。

constとidはそれぞれ以下のような型を持ちます。

ghci> :type const
const :: a -> b -> a
ghci> :type id
id :: a -> a

そしてconstの第一引数にidを渡すと、

ghci> :type const id
const id :: b -> a -> a

こうなります。
これがどうなっているのか分からず、しばらく悩みました。


色々試した結果、どうやら

const :: a -> b -> a

この型パラメータ'a'に idの型(c -> c)を入れると導出できるようですね。

-- とりあえずaの部分に(c->c)を突っ込んでみる
const :: (c -> c) -> b -> (c -> c) -- ??

第一引数はidを渡すので、実際には左の(c -> c)部分は消費されます。よって、

const id :: b -> (c -> c)

(->)は右結合なので

const id :: b -> c -> c

となると。
const idはflip constに等しいそうです。

一応QuickCheckで試してみます。

Prelude> :m Test.QuickCheck
Prelude Test.QuickCheck> let prop_const x y = flip const x y == const id x y
Prelude Test.QuickCheck> quickCheck (prop_const :: Int -> Int -> Bool)
+++ OK, passed 100 tests.

確かに通ります。