2013年10月29日火曜日

xmonadとHaskell(その8:関数の面白性質)

前回書いた、謎のカリーおじさんのイラストの話。
謎でもなんでもなくて、ハスケル・ブルック・カリーさんらしい。。。。

さて、本題のHaskellの関数の話。

括弧がなさ過ぎてよくわからない?

C言語なんかだと、関数の引数は括弧で囲まれている。
 
 hogefunction (arg1,arg2)

みたいな感じ。

しかし、Haskellにはそういう括弧はないので、初めて接する時にどれが引数なのかわからなかったり、そもそも関数がどれかもわからなかったりするかもしれない。上のコードはHaskell的には

 hogefunction arg1 arg2

みたいな感じになるが、この名前を単純にして

 a b c

と書かれると、なんだかわからなくなるかもしれない。


名前が単純でもC言語的に書けば

 a(b,c)

で、なんとなくわかる。

このわからなさを解決するアバウトな目線としては、単語が並んでいたら先頭が関数、それ以外は引数って感じで把握するのがよさげ。
だいたいでいえば、関数と引数の結びつきは、演算子よりも強い。
なので、何かの記号の間にある単語は関数とその引数と考えても大体間違いない。

あとは、関数を小まめに調べるという作業をしていると、なんとなく慣れてしまう。
そして、この「関数を小まめに調べる」という作業は実に重要。

xmonad.hsに限って言えば、xmonadのモジュールのリファレンスのページをみたりすること!

Haskellの関数と引数

なぜ、関数を小まめに調べることが必要なのか?
それは、Haskellの関数は、決まった引数の数をとる必要がないからだ。

例えば、C言語で引数を3個とるhogeという関数があれば

 hoge(arg1, arg2, arg3)

という風に使われる。
そして、この関数を勝手に

 hoge(arg1arg2)

と使うことはできない。

ところが、Haskellだと、

 hoge arg1 arg2 arg3

という関数は

 hoge arg1 arg2

と使ったり、

 hoge arg1

と使ったりできる。

なので、xmonad.hsとかで他人のコードを見た時、その書かれている引数がその関数で定義されている引数のすべてとは限らない。
だから、関数を調べることが重要になってくる。

部分適用

では、もともと3つの引数がとれる関数を3つより少ない引数で評価するとどうなるか?
結論から言えば、省いた引数を引数として、もとの目的の値を返すための関数を返す

例えば、前回見たwrap関数(XMonad.Hooks.DynamicLogモジュールのリファレンス)

  wrap :: String -> String -> String -> String

この関数は、3つの引数、左側区切り文字列、右側切り文字列、中身になる文字列をとり、加工された文字列を返す。具体的には、

  wrap "[" "]" "home"

と使えば"[home]"という文字列を返す

そして次に、この引数を一つ省いて

  wrap "[" "]"

として評価すると、中身になる文字列を(省いた引数)引数としてとって、加工された文字列(もとの目的の値)を返すという関数を返す

  kakko_wrap = wrap "[" "]"

と定義し、このkakko_wrap関数の引数に中身になる文字列を渡してあげれば

  kakko_wrap "home"

"[home]"という文字列が返る。


もちろん二つ省けば、引数が二つ必要な関数が返る。

 maedakekakko_wrap = wrap "["

 maedakekakko_wrap "]" "home"

とすれば"[home]"になる。




こんな感じで、ある関数の引数の一部分だけを決定して使うことを部分適用という。

前回defaultPPの中で定義されていた一行をもう一度見てみる。


 ppCurrent = dzenColor "#00ffaa" "" . wrap "[" "]"

赤字の部分を見ると、まさに上で見たwrap関数の部分適用!
当然予想できると思うが、dzenColorもリファレンスで調べてみよう。型シグニチャは、、

  dzenColor :: String -> String -> String -> String

やっぱり、dzenColorは引数を三つとって、文字列を返す関数だけど、ここでは、二つの引数のみ渡されているので、

 ppCurrent = dzenColor "#00ffaa" "" . wrap "[" "]"

この赤字の部分は、dzenColor関数が部分適用され、ひとつの文字列を受け取って、なんかの文字列を返す関数となっている。

カリー化

あの肖像画の挿絵の人がカリーさんで、その人がハスケルさんだったとはびっくり。
そもそも、ハスケルさんが言い出しっぺということ自体しらんかった。

と、それくらいにカリー化について知らなかった。
なので、巷では、「カリー化と部分適用が混同されてうんたらかんたら」という難しそうな話があるが、ここではそういう話はない。

「カリー化」って名前からして、自分で何かをカリー化するのかな?と思っていたが、そういうことではなかったってこと。

そして、型シグニチャの書き方は、そういう意味だったんだ!!ってのにちょっと感動できること。

初めに話した括弧だけど、慣れると括弧いらん、、、というより、無いほうが自然だったってこと。

についての、まさにチラ裏話。


そもそも「カリー化」という言葉は、「関数をカリー化する」というのではなくて、「Haskellでは、関数がカリー化されている」とかいう風に使うほうがわかりやすいっぽい。(Haskellでは、自分で何かをしなくても関数がもともとそうなってるから)

そして、「カリー化」とは、どういうことなのかというと、wikipediaのカリー化のページによれば

 複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引 数を取り結果を返す関数」であるような関数にすること

となっている。言葉で言えば、ややこしいけれど、さっきまで見ていた、wrap関数の部分適用を思い出してほしい。

複数の引数をとる関数として、wrap関数で具体的に見てみる。

wrap関数についての「もとの関数の最初の引数」は、左側の区切り文字列である。
そして、その最初の引数だけを与えて評価すると、「もとの関数の残りの引数を取り結果を返す関数」、すなわち、右側区切り文字と、中身の文字列を引数にして、出力文字列を返す関数、上の例のmaedakekakko_wrap関数を返す。

つまり、上のカリー化の条件に合致していて、wrap関数は既にカリー化された状態にある。というより、Haskellでの関数は勝手にカリー化されているのだ。

なので、「関数を頑張ってカリー化するぞ!!」ということは考えなくてもよいし、カリー化が何かを知らなくても大丈夫なのだ。

でも、知っていると面白いこともある。
それが、型シグニチャだ。

関数書式を表すのに一般的に必要なものは、関数名、引数、そしてその引数の型がわかればよいので、なんとなくそれらが並んでいればよいような気がするのだけれど、Haskellではなんかやたらと矢印が並んでいる。

  wrap :: String -> String -> String -> String

変な書き方だなぁ、、例えば、

 関数名:wrap
 引数:String String String
 戻り値:String

とかのほうがずっとわかりやすいのにとか思ってた。

しかし、それは大間違いだった!!

まずは、引数が一つの関数を考えてみる。

 hogefunc :: String -> String


文字列の引数をとって、文字列の値を返す関数。
そして、矢印は関数を表している。そう、矢印は関数なんです!!
これを肝に銘じて、引数が二つの関数を考えてみる。

 hoge2func :: String -> String -> String

いまのところ、まだ、初めの二つのStringは引数で、最後のStringが戻り値で、間の矢印は単なる飾りにみえてますよね。

そこで次に、目が覚めるようにシグニチャに括弧をつけてみます。

 hoge2func :: String -> (String -> String)

なんかちょっと見えてきませんか??

hoge2funcは、一つ目の引数を与えると、 (String -> String)というものを返す関数であるって風に見えますよね??
で、(String -> String)っていうのは、もとの関数の二つ目の引数をとって戻り値を戻す関数です。つまり、文字列の引数を一つ受け取って、ある関数を戻り値にする関数を表している。

そして、これってさっきまで話してたカリー化の話!!
Haskellの関数はカリー化されているので、複数の引数をとる関数に、元の関数の最初の引数だけを与えると、元の関数の残りの引数をとり結果を返す関数を返してくれるってことが表現されている。

つまり、矢印は関数を表していると同時に、その矢印の連続は、カリー化された関数的な表現だったりする!


さて、調子に乗って、引数を三つにしてみる。
そう、wrap関数だ。

  wrap :: String -> String -> String -> String


さっきまでと、少し違って見えませんか?

  wrap :: String -> (String -> (String -> String))


こんな感じにみえてきませんか??

なんだか狐につままれた感じかもしれないけど、おもしろいですよね!!
もう、型シグニチャの矢印が、引数の間に挟まってる単なる飾りには見えなくなったでしょ??


さて、最後に括弧の話。
一番初めに、かっこが無いのは慣れないとコードが読みにくいという話をした。

括弧自体の話は、括弧がありすぎるLispも訳が分からないし、慣れればどうってことないはず。

というより、慣れると、カッコがなくても結合規則があれば、見通せるっていうのが、これまた気持ちいい気がする。


そして、またwrap関数にもどるけれども、wrap関数はもともとある文字を、区切り文字で挟むための関数である。で、wrap関数自体は、区切り文字をカスタマイズできるんだけれども、区切り文字が決まってしまっていて、単に、その区切り文字で挟んだ文字列を返す関数を表す関数が、部分適用で表現できるっていうのが、、、なんか、凄く表現が自然に見える。


 wrap "[" "]"  ← この表現で関数を表してる。とってもCool!!




0 件のコメント:

コメントを投稿