top page > computer > haskell > web_lecture > for_programmer > layoutrule.html
更新日:
文責: 重城良国

構文: レイアウトルール

はじめに

case式やlet式、where節はインデントで構造を表現できる。Haskell 2010 reportの2.7節が概要を10.3節が正確な仕様をそれぞれ説明している。概要と仕様を学ぶが雰囲気がわかる程度で良い。

Haskell 2010 report: 2.7 Layout

Haskell 2010 report: 10.3 Layout

概要

どっちも

Haskellではレイアウトルールと明示的な{}と;との両方が使える。混ぜても良い。

レイアウトルールの仕組み

レイアウトから{}と;を追加する位置を計算する。

レイアウトルールの開始

レイアウトルールはキーワード

where, let, do, of

のあとに{が省略されると始まる。{を追加し次の語のインデントを記録する。

続き

空白行やより深いインデントは要素の続きを意味する。;は追加されない。

次の要素

インデントルールのはじめで記録されたのと同じ深さのインデントならば次の要素として解釈される。;が追加される。

終了

インデントが浅くなるとそのレイアウトルールは終了だ。}が追加される。

開始後すぐに終了

インデントルールがネストしたときに内側のルールが開始してすぐに終了することがある。インデントルールを開始するキーワードのあとの語がすぐ外側のルールのインデントと同じかまたはより浅いインデントのとき内側のルールはすぐに終了する。{}が追加される。

構文エラーによる終了

}を追加しないと構文エラーになるときは}の追加が試される。

インデントルールのなかの明示的な{}

明示的な{}のなかでは外側のインデントルールは適用されない。

仕様

正確な仕様がある。なんとなくの理解で十分だ。

前処理

それぞれ追加する。タブは8タブだ。左端から始まる語のインデントが1だ。0ではない。

関数L

前処理後のトークン列に対して以下のような関数Lを適用する。

L (<n> : ts) (m : ms) = ; : (L ts (m : ms)) if n = m
= } : (L (<n> : ts) ms) if n < m
L (<n> : ts) ms = L ts ms
L ({n} : ts) (m : ms) = { : (L ts (n : m : ms)) if n > m
L ({n} : ts) [] = { : (L ts [n]) if n > 0
L ({n} : ts) ms = { : } : (L (<n> : ts) ms)
L (} : ts) (0 : ms) = } : (L ts ms)
L (} : ts) ms = parse-error
L ({ : ts) ms = { : (L ts (0 : ms))
L (t : ts) (m : ms) = } : (L (t : ts) ms)
if m /= 0 and parse-error(t)
L (t : ts) ms = t : (L ts ms)
L [] [] = []
L [] (m : ms) = } : L [] ms if m /= 0

<n>

<n>は続く語のインデントを示す。mは現在のインデントルールのインデント値だ。nとmが等しければ(if n = m)、次の要素が始まるので;を追加する(; : ...)。nがmより小さければ(if n < m)、インデントルールのスコープが終了する。}を追加しmをひとつ削る(} : (L (<n> : ts) ms))。<n>がスコープのインデントの値より小さいあいだそれをくりかえす。それ以外、つまりnがmsの先頭の値より大きいかまたはmsが空のときは<n>は無視される(L ts ms)。より深いインデントが要素の継続を意味するということだ。

{n}

{n}はインデントルールの始まるときのインデントを示す。現在のスコープよりも深いとき(if n > m)または現在インデントルールのスコープ外にあるとき(L ({n} : ts) [])、{を追加しnを現在のインデント値とする(n : m : ms, [n])。現在のスコープと同じかより浅いとき{}を追加してから現在のスコープの動作を行う。

}

トークン}があったら現在のインデント値が0ならばそのまま}を残す。「現在のインデント値が0」ということは明示的な{}のなかにいることを示している。インデント値がそれ以外の値であれば暗黙の{に対する明示的な}ということになり、パースエラーとなる。

{

明示的なトークン{が入力されたなら現在のインデント値を0とする。

パースエラー

インデントが0でないスコープ、つまり暗黙の{}のなかにいるときに次のトークンでパースエラーとなるような場合には}を追加する。

そのまま

それ以外のトークン(L (t : ts) ns)はそのまま(t : (L ts ms))にする。

入力の終了

明示的、暗黙のどちらのスコープよりも外(L [] [])にあれば終了する。また暗黙のスコープ(if m /= 0)にあればそのスコープについては問題ない。明示的なスコープ(m = 0)のときはパースエラーとなる。

インデントルールを使わない書きかた

「ファイルの先頭がキーワードmoduleでも{でもないとき先頭に{n}を追加する」というルールがある。今まで定義と定義のあいだに;が不要であり、そのかわりに定義の続きを表現するのにインデントを深くする必要があったのはこのルールによってインデント値1のインデントルール内にいたからだ。ファイルの先頭のトークンが{であればこのトップレベルのインデントルールは無効となる。以下のような書きかたが可能だ。新しいファイルで試してみよう。

nooffside.hs

{
foo =
3
+
5; bar =
5
*
7
+
3
}

% ghci nooffside.hs
Prelude> foo
8
Prelude> bar
38

また明示的な{}のなかで新しいインデントルールを始めることも自由だ。{}のなかではスコープのインデント値が0になることを思い出そう。

{
baz a = case a of
0 -> 8
_ -> 15
}

まとめ

Haskellではレイアウトルールを使うことも使わないこともできる。明示的な{}や;の記述はコードを自動生成するときに便利だ。レイアウトルールは{}と;の追加という形で定義される。厳密な定義があるが感覚的に理解しやすいルールとなっている。いろいろと試してみて体で覚えよう。

「構文: 型シノニム」へもどる 「再帰関数とは」へ

正当なCSSです! HTML5 Powered with CSS3 / styling, and Semantics