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に等しいそうです。
- 参考:2009-12-08
一応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.
確かに通ります。