ラムダ式、関数の部分適用、カリー化

長くなった。。Real World Haskell―実戦で学ぶ関数型言語プログラミング P103 〜 P107 あたりの話。


記述に正確を期すようにすればするほど難しい言葉ばかりが並んでしまって何が何だか分からなくなってしまう。
分かりやすさを優先して多少はくだけた表現でもいいよね、と言い訳。例えば、

くだけた表現
a は多相型といって、様々な型を取りうるということを表しています。つまりどんな型でもいいよ、ってこと。
厳密な表現
a は多相型で、これはすべての型の上で全称修飾された型を表します。多相型の表現式は本質的には、型の族をあらわすものです。(http://www.sampou.org/haskell/tutorial-j/goodies.html を参考)

厳密な記述ももちろん必要。でも今の自分には高すぎるハードルである。


おさらい。

Haskell関数型言語と呼ばれている。その名の通り、「関数」が重要な役割を果たす。

では関数とは何か。
関数とは、入力された値をいろいろ加工して返す(出力する)ものである。入力があって出力がある。これが関数。
そしてある関数の出力は別の関数の入力となり、その出力は別の関数の入力となり・・・・・・、と連なって
プログラムが構成される。そんなのが関数型言語によるプログラム、だと思う。たぶん。

Haskell のもう一つの特徴として、型を重要視する、というのがある。ある関数を使いたいとき、まずどんな型なのかを調べる。
自分で関数を定義する場合もどんな型にするのかをまず考える。関数には入力(引数)と出力があるから実際には入力と出力の型ね。
型に厳密なおかげでバグの少ないプログラムを書くことができるのもメリット。(もっとメリットあるんだろうけど)


例) dropWhile の型
型を調べるには、ghci プロンプトから、:t 関数名 [Enter] とすればよい

dropWhile :: (a -> Bool) -> [a] -> [a]

dropWhile :: 以降が型の説明。最も右側の値が出力の型を示す。それ以外(つまり (a -> Bool) -> [a] )が入力(引数)
の型である。よって dropWhileは、(a -> Bool) と [a] の2つの型の引数を受け取り、[a] の型の値を返す関数であることが分かる。例 終わり。


ここまできてようやく関数の中身について調べる。dropWhile とは、第2引数であるリストに対し、リストの先頭から走査して、第1引数の条件を満たす間、その値を削除する。条件を満たさなくなった時点で残りのリストを返す関数である。具体的には、

*Main Data.Char> dropWhile odd [1,3,5,2,6,7]
[2,6,7]

リストの先頭からたどって、奇数である間(つまり、1、3、5に対して)それを削除する。よって残ったのは[2,6,7]

*Main Data.Char> dropWhile isSpace "   abc "
"abc "

リストの先頭からたどって、空白である間(つまり最初のスペース3つ)を削除。残ったのは "abc "(末尾の空白は残る)
おさらいここまで。

無名関数(ラムダ関数)

たった1つの関数だけでプログラムが完結することはないわけで、実際には複数の関数を組み合わせてプログラムを構成することになるでしょう。

少し複雑な例を。リストのリストの各項目に対して、あるリストを1つでも含んでいれば True を返す IsInAny を考える。(p104)
条件が複雑な場合、その部分を別の関数として切り出すのは様々な言語でよく行われる。

-- 使い捨て関数を使うやり方
import Data.List
isInAny needle haystack = any inSequence haystack
    where inSequence s =  needle `isInfixOf` s

このように、inSequence が中継役となる。言い換えればそこでしか使われない、いわゆる使い捨て関数である。
こういう使い捨ての関数って関数名を何にするか、とかで結構迷ったりするんですよね。そこで、使い捨て関数を使わないやり方として無名関数(ラムダ関数)というのがあります。Haskell 独自のものでは無く、今では C#JavaScript などでもおなじみですね。

-- ラムダ式を使う方法
isInAny2 needle haystack = any (\s -> needle `isInfixOf` s) haystack

引数の箇所に直接条件式を組み込むことでソース短くなりました。しかし、短くなったのはいいのですが、可読性が落ちたともいえます。あまり多用すると見づらいソースになる可能性もあります。


もっと良い方法は無いのでしょうか?


あるんです。

部分適用(部分関数適用)

結論から言うと、

-- 部分適用を使う方法
isInAny3 needle haystack = any (isInfixOf needle) haystack

と書けます。シンプルですね。右辺の (isInfixOf needle) と書けるところがポイントですね。
ここで型に目を向けましょう。 isInFixOf は、

isInfixOf :: (Eq a) => [a] -> [a] -> Bool

つまり、引数を2つ([a] と [a])受け取って Bool型を返します。(※ (Eq a) はとりあえず置いておく)

繰り返します。受け取る引数は2つです。

でも、(isInfixOf needle) という部分は isInfixOf が引数を1つしか受け取ってはいないように見えます。これはどういうことでしょう。エラーにはならないのでしょうか。


実は、isInfixOf は引数を1つだけ受け取ることもできます。いや、isInfixOf だけではなくすべての関数は1つだけ引数を受け取ることができます。受け取る引数が足りない場合、それは残りの引数の型を返す関数として動作します。(関数を返す関数という時点で戸惑いますが Haskellではよくあること)


いや、実はこの説明でも不完全です。もっと正確に言うなら、
Haskell のすべての関数は1つしか引数を受け取ることができません。
な、なんだってー

最初の dropWhile の例で、

(a -> Bool) -> [a] -> [a]

のうち、最初の2つが引数(入力)、一番右が出力と言いましたが正しくありません。見て分かるとおり、これってどこまでが入力でどこまでが出力なのかわかりにくいでしょ、どちらの区切りも -> なので。

  • > には一つの意味しかありません。-> の左が引数の型、右が返す型。それだけです。

dropWhile は2つの引数を受け取るように見えるけど実際は1つの引数を受け取って、1つの引数を受け取る関数を返します。
だから引数1つでもエラーになりません。

必要な引数より少ない引数を与えることで、それは関数を返す関数として働きます。
これを関数の部分適用といいます。別名「カリー化」です。
部分適用することで、使い捨て関数を書くことなく、かつシンプルな記述ができます。徐々に引数を適用することでパーツ分けされるというか、視認性も良いです。


これだけだと狐につままれたような気持ちなので、型に着目して再度検討。

isInfixOf :: (Eq a) => [a] -> [a] -> Bool

部分適用部分 (isInfixOf needle) に対して仮にneedle の値を"aa" とすると(needleのままだと型が決定できなくて後の説明が苦しかったので)

isInfixOf "aa" :: [Char] -> Bool

である。つまり文字列を受け取ってBoolを返す関数として働く。一方、any の型は、

any :: (a -> Bool) -> [a] -> Bool

第一引数 (a -> Bool) のところに (isInfixOf "aa") を入れても型的には矛盾しないので大丈夫。


結論としては、関数の部分適用のおかげで、複数の関数が組み合わさったような構成でもシンプルに書けるよ、ってことでいいのかな?
使い捨て関数を使うことなく、ラムダ式を使うこともなく。


疑問点

部分適用 = カリー化 でいいのだろうか。http://lambda-the-ultimate.org/node/2266 などでは 部分適用 != カリー化 みたいに書かれているけど。まだよく分からない。

後書き

引数や、-> の定義が二転三転したわけですが、そもそもそれらの意味というのを考えるからこのようなことになるのでないか、とか思ったり。形式論レベルと意味論レベルの混在というか。とはいってもそううまくいくものでもないけど。