2013年12月8日日曜日

xmonadとHaskell(その13:具体型と型コンストラクタ)

タイル型ウインドマネージャの楽しい処のひとつとして、レイアウトがある。

デフォルトのレイアウト「Tall」


デフォルトのキーバインドでは、M-Spaceでレイアウトが切り替わる。
「Mirror Tall」と名づけられたレイアウト


更にM-Spaceで、画面一杯に広がる「Full」レイアウト


xmonadにおいて、これらレイアウトは、「型」として表現されている。

XConfig l型データのリファレンスでいつものようにコンストラクタを確認して、layoutフィールドをみると「l Window型」となっている。
この型のパターン、いつも見ている「XConfig l型」に似た変な型だ。

そもそも、Int型やBool型のような「なんとか型」じゃない、「なんとか なんとか型」ってなんだろう??と思っている人もいると思う。

haskellの初心者解説をネットであちこち漁っていると、「なんとか なんとか型」の代表格である「Maybe a型」にまずは出会うかもしれない。
そして、さらに、「Map k a型」に出会ったり、解説によれば「実はリストも [] a型」だとかいう話も読んだかもしれない。

そこで、にわかhaskellerである我々は、解説を読みながら一生懸命イメージして理解しようとする「このわけのわからん型は具体的に何を表現しているのかな?」と。

Maybe a型は、Just aとNothingがあって、この型はエラーを含む戻り値を表現する場合に使えたりするとか、リストの話を聞いて、aの部分は任意の型であり、それを含んで表現される[a]というのは箱のような存在だとか頑張って想像してみたかもしれない。

やがて、そもそも、我々はxmonadのカスタマイズをするためにhaskellを読み始めていたりするので、遅かれ早かれ、「XConfig l型」とはなんぞや?と理解しようと努力するようになる。

そして、頑張って想像してみるに、「任意のl型データを入れてある箱のような存在がXConfigだ」とか。layoutHookフィールドの中にみつけた同じ「l」の字をもつ、「l Window型」に至れば、その任意のl型データがWindow型データの箱で、、、うにゃうにゃうにゃ、、、

さらに、不幸にもリファレンスからStackSetを覗き見たり、型クラスの領域に踏み入れたりしてしまうと、もっと大量の型引数がでてきてしまって、そっとページを閉じるのだ。「今はよくわからんが、haskell的センスが磨かれたら、想像出来るようになるにちがいない!!だって、haskellは難しいんだから、理解するのに時間がかかるんだもん」と、、、

言わずもがな、少し前の私だ。

しかし、上記の気弱な言い訳は見当違いのものだったりする。わからないのは、難しいからではなくて、知らないからなのだ。

「そもそも型とは何ぞや」という壮大な問いを一生懸命イメージしたりする以前に、型について初心者が知っておくべき超重要なことが存在する。

というよりも、このことを知れば、xmonadの設定レベルでは「そもそも、型とはなんぞや??」なんいうつまらない事で悩まなくなる。

なので、haskellの教科書的な本もまだ持たず、ネットのhaskell情報を断片的に読み漁りながら彷徨っているxmonadカスタマイザーの中でも無謀といえる猛者たちへ、haskellの型にもう少し親しみが持てるようになれるかもしれないメモをしたい。

型の種類


haskellの型に親しんでいくとき、まず第一に覚えなければならないことがある。

それは、型には種類があるということだ。

この型の種類は、一つが「具体型」と呼ばれるもの、もう一つが「型コンストラクタ」と呼ばれるものである。

まず、いわゆる「普通の型」だと思っていたものがだいたい「具体型」だ。
Int型、char型、String型、Bool型

次に、もう一つの種類、型引数をとるものを「型コンストラクタ」という。
いつも見ている「XConfig l型」であるが、「XConfig」の部分が型コンストラクタである。そして、「l」の部分を「型引数」という。

更に、型コンストラクタは必要な型引数が与えられると、具体型になるということを覚える。

すなわち、XConfigという「型コンストラクタ」にlという型引数を与えると、「XConfig l型」という「具体型」になるのだ。

この簡単な約束事を把握した上で、少し練習だ。
ソースコードの中の型を表現する項目のある部分に

 Hoge a

とかかれている部分があったとする。
上の説明を見た後だと、Hogeが何で、aが何で、Hoge aが何かわかるだろうか?





簡単?
正解は、


である。

すなわち、これだけだとわからないことだらけなのだ。
そして、この「わからないこと」がわかるようになると、色々とわかるようになるのだ。

まず、Hogeは、大文字のアルファベットで始まっており、後ろに引数があるので「型コンストラクタ」であることがわかる。

そして、型コンストラクタは「必要な型引数」が与えられると具体型になる。
この「必要な」という部分が超重要なのだ。

型コンストラクタの型引数の数


(その11)でみた連想配列を表現する「Map k a型」を思い出してみる。
上記の流れから言えば、Mapは型コンストラクタであり、k、aはそれぞれ型引数だ。
連想配列のキーとなるものの型がk、値となるものの型をaとして、これを型コンストラクタMapにあたえると、それは具体型「Map k a型」となる。
すなわち、Mapは二つの型引数を必要としている。

では、「Map k」と書いてあると、それは何であろうか??

なんか、よく似た事をどっかできいたことあるよね。
関数の部分適用の話だ。

実は、型コンストラクタは部分適用ができ、部分適用された型コンストラクタは、型コンストラクタを表す。

すなわち、「Map k」と言う表現は「型コンストラクタ」を表している。

さて、ここで「Hoge a」である。
そう、Hoge aという表示単独を見た場合、それが具体型を表しているとは限らないのだ。

なぜなら、Hogeは少なくとも型コンストラクタであるが、その必要とする引数の個数がわからないからだ。
もし、Hogeが引数をひとつとるのなら、Hoge aは具体型であるけれども、2個以上の引数を取るのなら、Hoge aは型コンストラクタなのだ。

型コンストラクタの型引数の種類


更に、Hoge aの「a」だ。
型の表現において、小文字アルファベットで始まる文字列で示される者は型変数と呼ばれる任意の「型」である。すなわち、「型」ということは、2種類あり、「具体型」であるとは限らず、「型コンストラクタ」かもしれないわけだ。

そう、今、あなたが「もしや??!!」と思った通り、型コンストラクタは、型コンストラクタの引数をとれる。関数の引数に関数が取れたのと同じだ。

いつも見ている「XConfig l型」の「l」は実は型コンストラクタだったりする。

わかるために注目すべき点

上記から言えることは、単純に、型の世界で大文字で始まっている文字列だけをみても、それがとるべき型引数の数を知るすべはなく、それが具体型か型コンストラクタかが分からないのだ。

しかし、実はこの「Hoge a」については意地悪な問いかけであって、実際には型の表現は次の様になっている。

data Fuga (Hoge a) b = ....

ここで、今一度、「ソースの中で型が表現される場所」に着目してみたい。
haskellのソースでは、型が表現される部分とそれ以外の部分が明確に区別されている。

上の様に型定義のdataキーワードに続くイコールまでの部分、そして、値コンストラクタのフィールド(引数)部分、さらに、型シグニチャに見られる::演算子の右側部分だ。

そして、ここには約束事がある。この部分に表現される型は、必ず具体型なのだ。

すなわち、「Fuga (Hoge a) b」は全体として必ず具体型なのだ。
ここでは、型表現の中でも括弧は用いられてそれがひとまとまりになる。

なので、Fuga型コンストラクタは、型引数を2つとること「だけ」は「わかる」のである。
(Hoge a)自体は、Hogeの必要とする引数の個数、もしくは、Fugaの取る引数の種類の情報がわからなければ、詳しくは「わからない」のだ。

また、先に「l」は型コンストラクタだと書いた。
XConfig l型のlayoutHookフィールドについてみると、

layoutHook :: !(l Window)

すなわち、::演算子の右側は「l Window」が具体型であり、「l」は型引数を一つとる型コンストラクタであることがわかる。
そう、::の右側が全体で具体型だと決まっているので、lは型引数を一つとる型コンストラクタであるということが分かるのだ。ちなみに、Windowは具体型か型コンストラクタかはこれだけだと「わからない」ことが分かると思う。

というわけで、型に付いても関数の時と同じように、こまめにリファレンスをみる必要がある。何を見るかというと、特に、型引数の数である。

また、ghci上の「:k」コマンドで型の種類が調べられる。

import XMonad.Core
:k XConfig

とか試してほしい。「*」が具体型を表現している。

関数の型シグニチャ


実はここまでの知識を活用すると凄く面白いことが見えるようになったりする。

関数の型シグニチャだ。
これも::演算子の右側に書かれていることをいつも見ているので知っているはずだ。

関数については、(その8)でメモした。
関数がカリー化されいることや、部分適用にも馴染んでいるならば、既に、引数の間に挟まれた矢印が、引数を区切る単なる飾りでは無く、「関数」を表現しているというのは何となく感じられるようになっていると思う。

しかし、もっと驚きの秘密があったりする。
関数が::演算子を使って型シグニチャとして表現されているのは、引数の型を表現するためではない。

実は、そもそも「関数」自体が「型」なのだ。

ここで、「関数が型ってどういうことよ?」といって頭を悩ませる必要はない。
単に今まで上で見てきた「型の表現」の話として以下を読み進めて欲しい。

例えば、足し算の関数として、add関数があったとする。
Intを引数に二つとって足して結果を戻り値にする。
それを型で表現すれば、以下のようになるだろう。

add :: Int -> Int -> Int


haskellの関数はカリー化されているので、以下の様に書き換えることが出来る。

add :: (Int -> (Int -> Int))


ここまでは、(その8)でメモした話だ。

ここからが、今回の話だ。

まずは、(Int -> Int)の部分に着目してみる。
矢印記号は、通常の関数における中置表記の様な存在だ。
なので、実は構造としては、二つの引数をとる関数と同じようなものであり、括弧でくるむと前置表記できたりする。

すなわち、こうだ

((->) Int Int)

ここで、「->」というのが型コンストラクタであり、この型コンストラクタが二つの具体型を型引数としてとるとすれば、、((->) Int Int)は、「(->) Int Int型」のデータとよめる。

同じように全体も書き換えられる。

((->)  Int ((->) Int Int))

(->)型コンストラクタが、Int型のデータと、((->) Int Int)型のデータを型引数としてとって具体型になっているのだ。

つまり、「関数」というのは、(->)型コンストラクタが型引数を二つとって作る具体型なのだ。

凄くない??
だけど、まぁ、xmonadの設定には何の関係もなかったりする。

レイアウトの仕組み


さて、そんなことよりもxmonadでのレイアウトだ。
その設定は、layoutHookフィールドに定義することになる。

さっきもみたが、

 layoutHook :: !(l Window)

である。
型の話だけで言えば、Windowを型引数にとって具体型になるものである。

で、これって、具体的になんなのか?

まずは、XMonad.Layoutモジュールのリファレンスを見てみる。
http://xmonad.org/xmonad-docs/xmonad/XMonad-Layout.html

そこに見慣れた文字がある、FullやTallやMirrorだ。

data Full a

と書かれている。あらためて型の定義を見ると分かることがある。
これは、Full型コンストラクタが型引数をひとつとって「Full a」型という具体型をつくるということだ。

もう一つ、

data Tall a

も同様だろう。ちなみに、「Tall」は、背が高いではなく、多分、TileのTとall windowのallじゃなかろうかとおもう。

更に、

newtype Mirror l a

というのがある。
newtypeキーワードは気にしないでみると、Mirror型コンストラクタは型引数を二つとって「Mirror l a」型という具体形をつくることはわかる。そして、これは、(Mirror l)型コンストラクタが一つの型引数をとって、「(Mirror l) a」型という具体形をつくるといいかえることができる。

すなわち、このどれもが「l Window」型に合致するのだ。

ここまでは、型の話だ。
では実際に、layoutHookフィールドではどうかくのか??
型定義のイコールの右側、値の話である。

Full a型の値コンストラクタはFullだ

layoutHook = Full

Tall a型の値コンストラクタは引数をとるので

layoutHook = Tall 1 3/100 1/2

最後のMirrorの値コンストラクタには

Mirror (l a)

と書かれている。これは、Mirror値コンストラクタは、「l a」型の値を引数にとるということである。その具体的な値は例えば「Tall 1 3/100 1/2」であるので、

layoutHook = Mirror $ Tall 1 3/100 1/2

と書く事が出来る。

さて、これらの書き方だと、単独のレイアウトしか使うことが出来ない。実際には、M-Spaceでこれら三つのレイアウトが切り替わったりするのだ。

どうするのか??
というより、defaultConfigではどう定義されているかいつものリファレンスからソースを覗き見てみよう。
http://xmonad.org/xmonad-docs/xmonad/XMonad-Config.html


layout = tiled ||| Mirror tiled ||| Full
 where
  tiled = Tall nmaster delta ratio
  nmaster = 1
  ratio = 1/2
  delta = 3/100


このlayoutがlayoutHookに定義される。

注目点は(|||)演算子だ。この演算子の説明は、XMonad.Layoutのリファレンスに見つけることができる。


(|||) :: (LayoutClass l a, LayoutClass r a) =>
       l a -> r a -> Choose l r a


(|||)は右結合演算子なので分解して、まず「Mirror tiled ||| Full」を型で表現してみると次のようになる。

 (Mirror Tall) a -> Full a -> Choose (Mirror Tall) Full a

(Mirror Tall)が型の部分適用で型コンストラクタとなり型引数「l」に対応だ。

そして、同リファレンスで紹介されている通り、「Choose」は型引数を3つとる型コンストラクタであることが、「Choose l r a」型の定義からわかる。
なので、「Choose (Mirror Tall) Full a」は「Choose (Mirror Tall) Full」を型コンストラクタ 、「a」を型引数として、分解でき、型変数を使った表現「l a」型に合致する。

更に、

(tiled ||| (Mirror tiled ||| Full))という式の型は

Tall a
-> (Choose (Mirror Tall) Full) a
-> Choose Tall (Choose (Mirror Tall) Full) a

と表される。
そしてこれもまた、型変数で表せば、「l a型」となっている。

この「l」というのは、任意だということは何となく知っていたと思うが、aを型引数としてとるために部分適用で表現された変幻自在の型コンストラクタだという驚愕の真実までは知らなかったと思う。

まぁ、実際には、(|||)の引数になるには、一定の制限(LayoutClass l a等)があるものの、レイアウト設定の仕組みは、型引数を一つとって具体型になる型を(|||)演算子でいくつでもつなげることが出来、しかも繋げた結果も同じ型だったりするので、その型の値をそのままlayoutHookに定義してあげれば良いようだ。


さて、ここであらためて、リファレンスで、defaultConfigの型シグニチャをみると

defaultConfig :: XConfig (Choose Tall (Choose (Mirror Tall) Full))

となっている。

このメモを読む前ならきっと見て見ぬふりをしていただろう。

しかし、今ならそこに書いてある意味がなんとなくわかったりするんじゃなかろうか?
というより、その文字の並びは上記で既に見覚えがある。

defaultConfigはXConfig l型の実際の値である。なので、layoutHookフィールドにも実際の値が定義される。ということは「l」の部分にもその値に基づく具体的な型が表れてくるのだ。

その実際の型は長ったらしいけれども、単なる一つの型引数をとる型コンストラクタのことなのだ。

0 件のコメント:

コメントを投稿