2013年11月25日月曜日

xmonadとHaskell(その11:キーバインドのしくみ)

xmonadでは、xmonad.hsを書き換えた場合、xmonadを再起動しなくても、デフォルトでは"mod-q"のキー操作で新しい設定に更新できる。

カスタマイズをする時に頻繁に使うキー操作だが、前回(その10)のカスタマイズをした後、このキー操作を行うと違和感を感じたりする人がいると思う。

更新のキー操作をするたびに、見た目では気が付きにくいが、実はステータスバーが重なって表示されていたりする。リロードする度にconkyのプロセスがどんどん増えてしまうのだ。

なので、"mod-q"で、単にxmonadのリロードを行うだけでなく、conkyのプロセスを終了する処理を加えるカスタマイズをメモしたいなと思う。

そのためには、まず、xmonadでのキー設定の仕組みに着目してみる。

いつものように、XConfig l型のリファレンスXConfig l型のリファレンスでコンストラクタをみる。
この中の、keysフィールドで、キーバインドの設定をすることとなる。

 keys :: !(XConfig Layout -> Map (ButtonMask, KeySym) (X ()))

ちょっとだけ見た目がややこしいので整理すると、keysに設定すべきものは矢印があるので、関数であることがわかる。
そして、落ち着いて分解すれば、この関数は、「XConfig Layout」型の値を引数としてとり、「Map (ButtonMask, KeySym) (X ())」型の値を返すような関数だと読める。

「XConfig Layout」型は、具体的に言えばdefaultConfigのような値であり、いつも見ているXConfig l型の具体的な値の事である。

で、問題は「Map (ButtonMask, KeySym) (X ())」型である。

タプルというデータ表現

haskellでは、データの表現の方法にタプルというものがある。

(1, 2)

("hoge", 2, True)

要素をカンマで区切り、丸括弧で囲んだものである。要素になるものの型は何でもよい。

ここで、データの表現で見慣れているリストと新しく見るタプルを比較して、その特徴をみることにする。

[1,2,3]

["hoge","hage","fuga"]

リストは型で表現すると「[a]」となり、いくつもの要素があっても全て同じ型である。そして、型からみれば、その要素の数はいくつでも構わなかったりする。

一方、タプルは、要素ごとに好きな型を取ることができ、上の例を型で表現すると

(Int, Int)

(String, Int, Bool)

である。そして、リストと違い要素の数は型の表現どおり固定されている。

ここで、問題の式を見ると、そこにタプルがある。

(ButtonMask, KeySym)

この部分がタプルだ。

タプルは見ての通り、何かの組み合わせを表現する時に便利。
このタプルは、ButtonMask型のデータとKeySym型のデータの組み合わせを表現している。

連想配列の型

さて、改めて「Map (ButtonMask, KeySym) (X ())」をみてみる。
複雑にみえるがまず、これは「」を表現しているということをしっかり認識すること。

これは、「Map k a」型と呼ばれる型であり、haskellで連想配列(ハッシュ)を表す型だ。具体的な値となるときには、kの部分にキーとなるものの型、aの部分に値となるものの型を書いてあらわす。

連想配列、連想記憶配列、ハッシュ、いろいろな風に呼ばれるが、何かのプログラム言語を触ったことがあればなんとなくイメージできるだろう。(連想配列のウィキペディア

簡単に言えば、名前をキーに、電話番号を値にみたいな感じでデータを管理する方法だ。
イメージ的には

("isono"    => "090-xxxx-0000",
 "fuguta"    => "090-xxxx-1111",
 "namino" => "090-xxxx-2222")

みたいな感じ。
電話番号を探すのに人の名前で探せる。まさに、連想だ。

haskellの話や本のなかで、map,zip,fmap等々にたようなのが出てきて(これらはまったくMapとは関係ない関数の話)初めの頃は混乱しがちになるので、整理しておくべき。

とりあえず、haskellで「Map」といえば、「連想配列」を表すデータのこと。
同時に、大文字のMで始まっていることに意識しすれば、型の話だと認識しやすい。

で、「Map (ButtonMask, KeySym) (X ())」のキーは、上でみたタプルで表現された型であり、(ButtonMask,KeySym)型である。そして、値は、X()型、すなわち、前にみたIOアクションのように、xmonad上で「実行される何か」の型である。
まさに、キー操作と動作の組み合わせで、キーバインドそのものだ。

以上をまとめると、

 keys :: !(XConfig Layout -> Map (ButtonMask, KeySym) (X ()))

というのは、XConfig Layout型のデータを引数にあたえると、キーバーインドの連想配列を返す関数を登録することになる。

ここで、キーコンフィグの話を進める前に、もう少し連想配列の話。

Data.Mapという連想配列のモジュール

上で出てきたのは、連想配列の型の表現の話だった。
今度は、実際に連想配列のデータを作ってみる。

まずは、そのために下準備が必要だ。
連想配列であるMapは、haskellのData.Mapというモジュールで定義されているのでそのモジュールをインポートする必要がある。

ここで、巷のxmonad.hsをみても、ファイルの初めに沢山のモジュールがインポートされているが、Data.Mapのモジュールのインポートは単純な

import Data.Map

ではなく、大概以下の様になっている。

import qualified Data.Map as M

これは、複数のモジュールの間で同じ関数名が使われている場合に混乱するので、「この関数を使うときには必ず、その旨を明記します」という約束書きである。

具体的には、Data.MapモジュールにあるfromList関数を使いたいとしたら、必ず、「M.fromList」と書く。先の「as M」の部分は短縮形を使うという書き方であり

import qualified Data.Map

とだけ書いた場合には、Data.Map.fromListという書き方になる。

xmonad.hsでみられる他の重複を避ける書式もみておく。

import Data.List ((\\), find)
これは、括弧内で示した関数のみをインポートする場合。

import XMonad.Hooks.DynamicLog hiding(ppWindowSet)
これは、上と逆でその関数のみインポートしない場合。

さて、xmonadのモジュールはXMonadではじまり、そのリファレンスのページは、XMonadのドキュメントのページからみれるが、Data.MapはXMonadのモジュールではない。
そんな時は、とりあえず、HoogleのページにData.Mapとか入れて検索してみるとよさそう。

連想配列を作る

連想配列データは次の関数で作ることが出来る。

 fromList :: Ord k => [(k, a)] -> Map k a

キーと値の組み合わせをタプルで表し、それをリストにしたものを引数として渡す。すなわち、fromList [(キー, 値),(キー, 値),(キー, 値),...]見たいな感じ。

厳密に言えば、タプルのリスト、すなわち、[(キー, 値),(キー, 値),(キー, 値),...]、そのものが、連想配列なのではない。fromListからの戻り値が連想配列、つまり、それがMapと呼ばれるものだ。

上に書いた名前と電話番号のイメージを具体的なプログラムにしてみると次の様になる。


defaultConfigのkeysフィールド

XConfig l型の具体的なデフォルトデータ、おなじみのdefaultConfigのkeysフィールドを見てみよう。

http://xmonad.org/xmonad-docs/xmonad/XMonad-Config.html

このページの右隅にソースへのリンクがある。

該当部分の出始めの数行をコメント抜きで引用して、後を省略すると次のような感じになる。


戻り値になる連想配列が、上で話したM.fromListを使って作られているのが見て取れる。

さて、keysフィールドに設定すべきものは関数である。
キーバインドの設定をするなら、連想配列でデータを表現できるので、関数ではなく単なるMap k a型の連想配列データそのものでも良い様な気がするが、関数なのだ。

これは、xmonadの設定のパターンの一つであり、その理由は、連想配列を作るための一つ目のタプルを見てみるとすぐに分かる。

((modMask .|. shiftMask, xK_Return), spawn $ XMonad.terminal conf)

この連想配列は、modキー、シフトキー、リターンキーの同時押しで、ターミナルウインドウを起動することを示している。

例えば、modキーはwinキーに決まっていて、ターミナルはxtermに決まっているのなら、

((mod4Mask .|. shiftMask, xK_Return), spawn "xterm")

として、固定的な連想配列だけを作れば良い。

しかし、ここで、思い出して欲しいのは、最小限のxmonad.hs


つまり、modキーは、XConfig l型データのフィールドの一つ、modMaskフィールドでカスタマイズ可能だ。また、ターミナルウインドウもterminalでカスタマイズ可能だ。

なので、xmonadの意図に沿うなら、カスタマイズされたXConfig l型データを通じて、ターミナルウインド起動の時には、modキーが何に設定されているか、ターミナルウインドに何を使うのかを知る必要がある。

ここであらためて、keys関数の引数は何なのかというとXConfig Layout型である。

これは実際にxmonad関数に渡される値であり、xmonadはキーバインドを行う時に、xmonad関数に引数として渡された今私たちがカスタマイズしているXConfig l型データを、keys関数に引数として渡し、その結果得られる連想配列でキーマッピングを行っている。

わざわざ関数になっているのは、連想配列作成時に、カスタマイズデータを織り込むことができるようにする仕組みなのだ。

たぶん、他のプログラミング言語だと、グローバルな変数名で受け渡しする話なのかもしれないけれど、こういうところが、haskellの関数型言語っぽい作法なのに違いない。

実は、設定データを引数で渡すという方法は、あちこちで見られるパターンだ。
(その10)で紹介したxmonad.hsの中でも、「my_dzen_PP h = ...」と定義している部分は、まさにこれと似ている。PP型のデータ自体は、固定的だが、書き出しハンドルは動的なので、その部分のデータを知る必要がある。そこで、ハンドル(h)を引数として受け取る関数を作っている。

パターンマッチ

 keys conf@(XConfig {XMonad.modMask = modMask})

上は、kesy関数の定義の一部だ。
「keys」が関数名で、「conf@(XConfig {XMonad.modMask = modMask})」が引数。

haskellの関数定義においては、引数にパターンマッチとよばれる仕組みがある。

例えば(その2)で定義したデータ型

ここで、このHumanデータ型を引数にとって挨拶するhello関数を作ってみる。型シグニチャは以下の通り。
(値コンストラクタがOkamaの場合のみ。nameフィールドから名前を取り出してその名前に挨拶する文字列を作る。)

hello :: Human -> String

この関数を色々な仮引数で定義して、代数データ型のパターンマッチに親しんでみる。
hello dareka としたときの結果はすべて"hello, tubasa"だ。



・その1

 hello h = "hello, " ++ (name h)

Human型そのままを仮引数とするパターン。名前の取り出しは、レコード構文で自動的に作られるフィールド名の関数で取り出す。

・その2

 hello (Okama n i) = "hello, " ++ n

値コンストラクタに引数を与える形のパターンマッチのパターン。仮引数nが、名前を表すものとなる。フィールドは引数の位置によって識別されるので、値コンストラクタの引数は全てを仮引数として表現しないとダメ。

・その3

 hello (Okama{ name = n}) = "hello, " ++ n

値コンストラクタをレコード構文の形でパターンマッチするパターン。仮引数nが名前を表すものになる。フィールド名で特定できるので、必要なものだけ仮引数として表現。

そして、最後に「asパターン」と呼ばれるパターンがある。1と3の合成だ。

hello h@(Okama{name = n}) = "hello, " ++ n ++ show (saikyou h)  

@の前の部分がその型全体を表現する仮引数(その1)、後ろの部分が値コンストラクタのパターンマッチ(その3、または2でもOK)
このhelloは、名前の後ろにさいきょう度がつく。

というわけで、keys関数はこのasパターンであり、仮引数は赤字の部分

 keys conf@(XConfig {XMonad.modMask = modMask}) 

confがXConfig Layout型の値全体を、modMaskがXConfig Layout型のmodMaskフィールドで定義された値を指している。

このmodMaskっていう仮引数名の付け方は紛らわしいことこの上ない。
フィールド名称と同じであるし、具体的には、連想配列定義の以下の赤字、青字の部分

 ((modMask .|. shiftMask, xK_Return), spawn $ XMonad.terminal conf)

がこの引数なんだけど、一見すると、「modMask」自体が「ButtonMask型」に定義されている、値なのかと見間違えそう。

ちなみに、うしろのconfが値全体のconfで、「XMonad.terminal conf」がお気に入りのターミナルを表す例のアレだ。




2013年11月20日水曜日

xmonadとHaskell(その10:ManageDocks)

(その4)辺りで「ステータスバーを付けてみる」と言いつつダラダラとメモしてきたので、いい加減ここらで具体例。

いつもの見慣れたスクリーンショットの様なステータスバーを付ける。


xmonadでステータスバーを表示するには外部アプリを利用する。
ここでは、表示用に「dzen」、情報取得用に「conky」。

ステータスバーは画面の上部に表示するが、このステータスバーは左右二つのdzenから構成されている。左側がxmonadからの出力を表示しているdzen、右側がconkyからの出力を表示しているdzen。


まずは、xmonad.hs
外部アプリの起動は、do構文を使ってmainで行う。

8行目でxmonadからの出力を表示するdzenを起動。spawnPipeを使ってハンドルを得る。

一方、9行目はconkyからの出力を表示するdzen。conky出力をパイプでdzenに繋いだシェルコマンドをそのまま実行するので、ハンドル(結果)は必要ない。そこで、これの実行については、spawnPipeではなく、spawnを使っている。

spawnPipe等に渡す引数はシェルで実行する文字列をそのまま文字列として渡せばよい。

dzenコマンドの引数の概略(詳細はhttps://github.com/robm/dzen

 -x  左端表示位置
 -w  長さ
 -ta 内容の表示位置、右寄せならr、左寄せならl

以下のものは左右のdzenともに共通なので18行目でdzen_styleとしてまとめて定義

 -h ステータスバーの高さ
 -fg フォアグランド色
 -bg バックグラウンド色
 -fn フォント名

xft用(?)のフォント名は「fc-list」コマンドを使ってシステムで使えるフォント名を調べる。サイズの指定は、そのフォント名にコロン「:」で繋げて「size=10」とかする。
また、普通のフォントの名前なら「xfontsel」コマンドとか使って調べられる。

そして、conky(http://conky.sourceforge.net/)
ネットで検索すると、デスクトップ上にグラフィカルに表示されたかっこ良いシステムモニタの画像がたくさん出てくる。

しかし、ここでは、この表示能力は使わず、その情報収集の機能のみを利用する。
上記xmonad.hsの9行目 "conky -c ~/.xmonad/conky_dzen_laptop" として呼び出している。
このconky_dzen_laptopの内容は以下の通り。
尚、/neko/home/.xmonad/icon/ディレクトリ内のアイコンファイルは、もともとdzenのサイトで配布されていたみたいなんだけれど、現在の配布は不明。しかし、githubで管理されているxmonad.hsの中によく入っているので適当に検索すると拾える。

さて、今までメモしてきたことは、dzenというアプリを画面上に表示してそこに情報を表示するということでしかなかった。

しかし、ステータスバーとして機能するためには、その表示用アプリであるdzen上にウインドウが重ならず常に見えていることが必要だ。

それを実現するための機能がxmonadには用意されている。

XMonad.Hooks.ManageDocksモジュール

このモジュールには3つの便利な機能が用意されている。

manageDocks

xmonadには、manageHookという機能がある。
簡単に言えばこれは、xmonad上でアプリケーションが実行されて新しいウインドウが開いた時、そのウインドウをどんなふうに扱うかを操作するための仕組みだ。
例えば、「firefoxが起動されたら、そのウインドウをwebというワークスペースに移動させる。」とか、「gimpが起動された時には、フローティング配置(タイリングしない)する」とかである。

この機能の設定は、XConfig l型データのmanageHookフィールドで行う。

で、ステータスバー的に振る舞わせたいウインドウ、すなわち、他のウインドウのようにタイリング表示されたり、特定のワークスペースだけに表示されたりするのではなく、常に一定の同じ場所で表示され続けるウインドウとして扱いたい旨の指定を簡単にしてくれるのが、このモジュールのなかの「manageDocks」という値だ。

上記xmonad.hsの15行目

manageHook = manageHook defaultConfig <+> manageDocks

XConfig l型データのコンストラクタをみるとmanageHookフィールドに設定するものはManageHook型のデータである。

ManageHook型データそのものの具体例については、別の機会にメモするとして、上の式の意味をみてみる。

式の読み方はだいたい、「manageHook defaultConfig」と「manageDocks」を演算子「<+>」で繋いだものであるという感じ。

まず、「manageHook defaultConfig」は、(その3)でもメモした「レコード構文で定義したフィールド名(項目名)は、その型のデータからそのフィールドの値を取り出す関数になる」というアレである。
つまり、defaultConfigとして定義されている値のmanageHook項目に入っている値を取り出している。そして、当然その値の型はManageHook型だ。

次に、「manageDocks」は、上で書いた通りステータスバー等の制御のためのデータが入ったManageHook型の値だ。

そして、演算子「<+>」はManageHook型の値を結合してひとつのManageHook型の値にするものである。

なので、15行目では、defaultConfigで定義されているmanageHookの内容に、manageDocksの内容を追加したものになる。

avoidStruts

xmonadには、layoutHookという機能がある。さまざまなタイリング、フルスクリーン、タブ等など。
おなじみのレイアウトであるが、どんなレイアウトを使うかは、XConfig l型データのlayoutHookフィールドで行う。

ステータスバーを使用する場合、ステータスバーの部分に他のウインドウが重ならないように配置を制御しなければならないが、このレイアウト制御をしてくれるのが「avoidStruts」だ。

上記xmonad.hsの14行目

layoutHook = avoidStruts $ layoutHook defaultConfig

layoutHookに定義すべきデータは(l window)型で、この型自体については、また別機会にメモ。
とりあえず、式の構造をみてみると、「avoidStruts関数」に「layoutHook defaultConfig」を引数として渡した戻り値となっている。

まず、「layoutHook defaultConfig」は先と同じで、layoutHookが関数であり、defaultConfigで定義されたlayoutHookフィールドの値が戻り値となる。

そして、avoidStrutsは関数であり、レイアウト制御のデータをとって、それに、ステータスバー用の隙間を開ける加工をしたレイアウト制御データを返すというものである。

docksEventHook

リファレンスの説明から具体的なイメージがよくわからないので、今回のxmonad.hsには含めてないが、使い方はmanageDocksと同じパターンで、XConfig l型データのhandleEventHookフィールドに定義すればよさげ。

handleEventHook = handleEventHook defaultConfig <+> docksEventHook

みたいにすれば、デフォルトのイベントフックにこの機能が追加される。


レコード構文を使った設定
初心者のころは、レコード構文のフィールド名が、データ型からそのフィールドの値を取り出す関数である、すなわち、同じ単語を使ってるけど意味が違うってのを知らないので、xmonad.hsが読めなかったりする。

しかし、これを理解して、改めて実際のxmonad.hsでの設定をみるとレコード構文の便利さが実感できる。

なんにせよ、xmonadの設定のあちこちでこれに似たパターンは使われるので

my_data = hoge_default { field_a = add_func $ field_a hoge_default }

みたいな形の理解は必須だ。

上下にステータスバー

スクリーンショットでもよく見られる上下のステータスバーは簡単にできる。
dzenコマンドに渡す引数の配置パラメータを少し変えて、左側dzenを上に、右側dzenを下にする。

8行目、-w 400をはずす

left_bar <- spawnPipe $ "dzen2 -x 0 -ta l " ++ dzen_style

9行目 -x 400 を変更して、-y 582を追加

...省略 | dzen2 -x 0 -y 582 -ta r " ++ dzen_style



しかし、S101はもともと画面の高さが低いので、両方のステータスバーを常に表示するとメインの表示領域が少なくなるから嫌だ!と考えたとしよう。

そんな時に役に立ちそうなのが、同じもジュール内にある「avoidStrutsOn」関数だ。上下左右のステータスバー領域を個別に設定できる。

avoidStrutsOn関数の一つ目の引数は、ステータスバーのための余白を開けたい方向を示す値(上はU、下はD、左はL、右はR)を要素としたリストとなり、二つ目の引数でレイアウトデータを渡す。

なので、上下にステータスバーがあるが上だけ常に表示したい場合は、以下の通り。

layoutHook = avoidStrutsOn [U] $ layoutHook defaultConfig



この方向を示すデータは、Direction2D型の値で、UとかDとかはいわゆる値コンストラクタだ(TrueとかFalseみたいなもの)。

Direction1D型のNext、Prevとともにxmonad.hsのカスタマイズのなかでよく出てくるのでリファレンスをチェックして頭の隅においておこう。
XMonad.Util.Typesモジュール

実際のところ

実際のところ、dzemanageDocksやdocksEventHookが効果出てるのかとかわからない。
まぁ、よそのxmonad.hsにはたいがい書いてあるので、おまじない的につけておけばOKなのかな。

それより、dzenの位置が画面の端から離れてると、avoidStrutsが効かないようなので注意。例えば、上で紹介した上下バージョンのステータスバーで下側のdzenの位置を -y 580とかするとavoidStrutsしても余白を作ってくれなくなる。

ついでに$演算子

$演算子は、割とどこでも説明されている「括弧を少なくするための記号」的なものだ。
解釈は、$の位置から最後までを括弧で囲んだのと同じ。なんて言う風に紹介されている。

つまり、上で紹介した

layoutHook = avoidStrutsOn [U] $ layoutHook defaultConfig

は、

layoutHook = avoidStrutsOn [U]  ( layoutHook defaultConfig )

となる。まぁ、そのおとりで簡単!

しかし、ここで、脱初心者的に$演算子の型シグニチャをみる。

($) ::  (a -> b) -> a -> b

まずは、$の左側(一つ目の引数)は関数で、右側(二つ目の引数)は「その関数に与える引数」が引数になるってこと。
そして、この演算子は右結合であるとともに、優先順位が最低なのだ。

すなわち、優先順位が最低ということは、必ず$記号が区切りとなるし、右結合なので、$のより右側が先に扱われるようになる。

だから、

a b c $ d e  $ f g $ h i

は、

a b c $(d e $(f g $(h i)))

になるとか聞くと、「へ~~~」ってなる。

ちなみに「$の位置から最後までを括弧で囲んだのと同じ。」という解釈は上手くいかなくて困ることもあるかもしれない。

例えば合成関数のときだ。

a . b . c $ d e

は、

a . b . c (d e)

という意味ではなかったりする。

何が違うかというと、(.)は右結合で、まずcを見ることになるが、その時、.演算子より空白結びつきが強いので

a . b . (c (d e))

となり、意図したのと違うものとなる。

実際には、$演算子を使うとその記号部分で区切られる(結合が弱い)ことになるので、あえて括弧を付けるなら

(a . b . c) (d e)

という感じになる。

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

となって括弧が外れる。

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