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」がお気に入りのターミナルを表す例のアレだ。




0 件のコメント:

コメントを投稿