curry/uncurry 関数の使い方がわかった(かも?)

∧ を再定義する演習に取り組んでいるときに、検算を map で実行しようと考えた。
∧ を and という名前の関数で定義したとして、これは二引数の(見かけをもったカリー化された、 Bool → Bool → Bool の型を持つ)関数になる。

一方、ふたつの Bool 変数の全組み合わせのリスト bs は

bs = [(a,b) | a <- [True,False], b <- [True,False]]

で定義できる。

ここで and は(Bool 変数を受け取って、 Bool 変数から Bool 値を求める関数を返すという形に)カリー化されているので、いちどきに Bool 変数の組を受け取ることができない… すなわち、このままでは直接 map 関数と bs で検算できない。

and 関数を Bool 変数をひとつずつ受け取る型から、 Bool 変数の組をまとめて受け取る型に変換できれば… ということで、出てくるのが uncurry 関数。 uncurry 関数は、変数をひとつずつ受け取る関数を数珠つなぎにした(つまりカリー化された)関数を、変数をまとめて受け取るカリー化されていない関数に変換する関数。
uncurry を and に適用すると、関数の型は (Bool, Bool) → Bool になる(uncurry and :: (Bool, Bool) → Bool)。

この uncurry and を map 関数で (ふたつの Bool 変数の全組み合わせのリストである) bs の各要素に

map (uncurry and) bs

として適用すると(and の実装が正しければ) [True,False,False,False] が得られる。

手続き型言語では、型のあわない関数とデータがあると、データをいじって関数が受け取れるように書き直すけれど、関数型言語では受け取る関数の側を変えて… という試行錯誤ができるところが面白い。

蛇足だけれど、この and と map, bs の例でデータの側をいじるというのは、組となってわたってくるデータを、ふたつに分けて and に渡す処理を書くというので、

map (\(a, b) -> and a b) bs

ようなラムダ式を書くことに相当するのだとおもう。