2013年11月12日火曜日

xmonadとHaskell(その9:演算子とか結合規則とか)

xmonadのある風景


デスクトップPCの広い画面だと、タイリングの便利さが実感できる。


さて、(その8)で関数の部分適用の話をして、dzenPPの項目の一つ

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

の部分のdzenColorやwrap関数の部分が部分適用された新しい関数ということを把握した。

ちなみにdzenColor関数の引数は、一つ目がフォアグラウンドの色、二つ目がバックグラウンドの色、3つ目が中身の文字列となっている。

dzen上での表示は通常テキストで行うが、一定のコマンド文字列を含ませることで、テキストに色を付けたりできる。このdzenColor関数は、そのコマンド文字列を簡単に生成してくれる。

READMEの「(5) In-text formating & control language:」辺りを参照

関数の合成

前回話題に上らなかった関数と関数の間にある「.」(ピリオド)は、関数を合成する演算子。

  g・f(x) = g(f(x))

数学的に書くと上みたいな感じらしい。

あまり難しく考えなくても、単純に、ある関数を適用して、その答えに別の関数を適用して、その答えにまたまた別の関数を適用して、、、という処理をする時、それらの各関数を「.」ピリオド演算子で繋げると合成された関数が返される。

ここで整理しておくべきことは、合成関数が、「関数の合成の話」であるということ。
これは、ピリオド演算子の両側にくるものは「関数」であり、戻り値も「関数」であると常に意識しておくことだ。そうすると、一見ややこしく見える式に出会った時のヒントになるかもしれない。


以下には、この整理の手助けになるかもしれないし、ならないかもしれない例を示してみる。

おなじみのppCurrentの式


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


これを簡略化し引数が単純なものを以下に示す。

func1 ::  String -> String
func2 ::  String -> String
resfunc ::  String -> String

resfunc = func1 . func2

これは、func1関数とfunc2関数を使って、resfuncという関数を定義している。


では次に、値を計算する時に、合成関数の書式を使うとどうなるか?

arg = "hogehoge"
res = func1 . func2 arg

と書きたくなる?


でも正解は下のような感じ。

res = (func1 . func2) arg

(func1 . func2) という関数にarg引数を渡すという風に表現する。


ピリオド演算子は、関数を合成する話、すなわち、合成した関数を作る話だったりする。
単に、関数を適用して求めた値に、更に関数を適用して値を求めるならば、

res = func1 (func2 arg)

となるのであり、これと合成関数の話は頭の中で整理して、区別する必要がある。


さて、合成関数の話はここまでにするとして、

では、何故、

 res = func1 . func2 arg

は、うまくいかないか??

「7+7÷7+7×7-7」の答えが分かる人は頭がいいらしい

っていうのが、ツイッターで話題になってたけれど、プログラムやってる人は、これが頭の良し悪し(何が正しくて何が間違っているか)とは無関係なことを知っている。

そう単に、優先順位の約束事がどう決められているかという問題だ。


(その8)で少し触れたけれど、haskellでは演算子よりも関数と引数が強く結びつく。(記号よりも空白の結びつきが強い)

なので、

res = func1 . func2 arg

は、

res = func1 . (func2 arg)

という風に扱われる。

そうすると、ピリオド演算子の後ろ側の型は関数でなく上の例では文字列になるので上手くいかなくなったのだった。

強い結合と弱い結合

巷で見られる結合についての解説では、結合が強いとか弱いという言い回しがよく見られる。
「強い」というのは、上の表現で括弧で括ったように func2  と arg がひとまとまりになっていると考えるので直観的で分かりやすい。

一方、「弱い」結合というのは、そこで「区切られる」という風にイメージする感覚に近い。

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

の = と . に色を付けてあるが、まず記号部分に注目して、そこで区切られるというイメージ。

こうイメージすると、その裏返しで、それ以外の部分がひと塊になっている事(強い結合)がイメージしやすい。


右結合と左結合

結合の話の中で「右結合」と「左結合」という単語も出てきたりする。

a * b + c * d

という式があれば、*演算子の各掛け算を先にして、その後で、それを足すという順番を考えたりする。
しかし、全部同じ順位の演算子、例えば、

a + b + c + d

という式があった時、各項目についてどのように着目しているだろうか?
なんとなく自然に、

まず、aとbを足して、
次に、その答えにcを足して、
更に、その答えにdを足すという感じだと思う。

括弧を付けて表現するなら

(((a + b) + c) + d)

こんな感じに着目していくことになっていたりする。
こういう演算子を「左結合」という。

演算子の左側をまず先に計算するイメージだ。
深く考えなくても、普段無意識に注目している通り、左から右へというイメージが「左結合」だ。
但し、この話の意識すべきは「演算子」の話であること。そして、順位が同列の演算子が複数ある時の話であることだ。


では、「右結合」ってどんなのだろう?
実は上で見た、合成関数を作る「.」(ピリオド)演算子が右結合の演算子だ。

a . b . c . d

という合成関数がある時、どういう順序で着目されているかというと

d関数にc関数を合成し、
その合成された関数にbを合成し、
その合成された関数にaを合成する。

となっている。
補助的な括弧を付けると

(a . (b . (c . d)))

こんな感じだ。
ちょうど上で見た足し算の時と括弧の付き方が逆である。
つまり、「右結合」は演算子の右側をまず先に計算する。複数の演算子があれば、より右側の演算子を先に結び付けて計算するイメージだ。


さて、この「右結合」、実は既に(その8)でも見ていた。

wrap関数の型シグニチャは

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

という風に表されるが、実は一つの引数をとって、関数を返すカリー化の性質を表現しているという話をして、補助括弧を以下のようにつけた。

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

これは、まさに右結合。
関数の型シグニチャの意味がhaskellを初めて見た時に分かりにくいのは、右結合の演算子で表現されているせいもあると思う。

しかし、右結合とか左結合の話をなんとなくでも把握すると、頭の中で瞬時に上の補助括弧が付けられるとともに、部分適用時に戻り値が関数となる様が見て取れるようになるかもしれない。

そもそも、「右結合」っていうルールを作ってあるのは、自然な左結合だけの世界だと、上で見たwrap関数の表現のように括弧をいくつもつけなければならない。

しかし、関数の表現は全部このパターンなのに、いちいち括弧を書くのが煩わしいので、「->」演算子は右結合というルールを作って括弧を外したらしい。合成関数の演算子も同様だ。


セクションとか、中置関数とか

さて、ちょっと演算子の話に戻って、いくつかの豆知識。

「+」等の演算子は、

1+2

とか日常で見慣れた並び方で使われる。

しかし、演算子も実質的には、二つの引数をとる関数である。
haskellでは、演算子を括弧で包むと関数的な表現ができる。

1 + 2

は、

(+) 1 2

と書くことができる。
また、演算子の型シグニチャをghciで見る時には、この括弧付きの形で表現しないといけない。

 :type (+)

括弧を付けづに

 :type +

としたら、エラーになる。

さて、(+)もhaskellの関数であり、カリー化されているので部分適用ができる。
しかし、演算子を括弧で包んだ関数の部分適用は、ちょっと変わっている。

普通の関数のパターンなら

(+) 1

とすることで、部分適用する。
しかし、演算子を関数化している場合には、引数を括弧の中に書いてしまえる。

(1+)



(+1)

こういう表現は特にセクションと呼ばれている。


一方逆に、普通の関数を演算子的に使うこともできたりする。これは中置関数と呼ばれる。
引数を二つとる関数について、「+」等の演算子のように引数と引数の間に関数名が並ぶような感じだ。

その方法は関数名を「`」バッククオートで囲ってやること。

add :: Int -> Int -> Int

 というような二つの引数をとって和を返す関数があった時

add 1 2

は、

1 `add` 2

と書くことができる。


巷のxmonad.hsでdefaultConfig{}の後ろに

`additionalKeysP`

なんて言うのがよく見られるが、これがまさにそうだ。

普通の関数で書けば

additionalKeysP defaultConfg {} hogehoge

という形になる。


中置関数は、視覚的な理解しやすさとともに、演算子化することで、結合の強さが変化し、括弧が不要になるという効果もある。


例えば、上のaddの場合、引数に別の関数hoge、fugaがある場合

add (hoge 1 2 3 )  (fuga 1 3 4)

という表現になるが、中置にすれば

hoe 1 2 3 `add` fuga 1 3 4

となって括弧が外れる。

まあ、日曜プログラマ的に言えば、やっぱり括弧は、わかりにくくならない程度に明示したほうが良さげなんだけれど。


0 件のコメント:

コメントを投稿