字句構造

この章では、Haskellの低レベルな字句構造を説明する。このレポートを初めて読む場合には、詳細はほとんど読み飛ばしてもよいだろう。

表記法

以下の表記法は構文を表すために使用される。

[pattern] 任意
{pattern} 0、またはそれ以上の繰り返し
(pattern) グルーピング
pat1 | pat2 選択
pat(pat') 相違 ー pat'によって生成されたものを除いた、patによって生成された要素
fibonacci タイプライターフォントの終端構文

このセクション内の構文は字句構造を説明しているため、全ての空白は明示的に表現されているが、並置されたシンボル間には暗黙的な空白はない。BNFのような構文は今後使われ、次の形式を取る。

nonterm alt1 | alt2 | … | altn

通常は文脈によって区別が明確になるが、 |[…]のような(タイプライターフォントで指定された)具体的な終端構文から|や[…]のようなメタデータ構文の区別には注意が必要である。

HaskellはUnicode[2]文字セットを使っている。しかしながら、プログラムソースは現在、以前のHaskellバージョンで使われていたASCII文字セットに偏っている。

この構文はUnicodeコンソーシアムによって定義されているUnicode文字の文字符号化スキームによって異なる。Haskellコンパイラーは新しいバージョンのUnicodeが利用可能になるにつれてそれらを利用されることが期待されている。

字句プログラム構造

program {lexeme | whietespace}
lexeme qvarid | qconid | qvarsym | qconsym | literal | special | reservedop | reservedid
literal integer | float | char | string
special ( | ) | , | ; | [ | ] | ` | { | }
whitespace whitestuff {whitestuff}
whitestuff whitechar | comment | ncomment
whitechar newline | vertab | space | tab | uniWhite
newline return linefeed | return | linefeed | formfeed
return キャレッジ⏎
linefeed 改行
vertab 垂直タブ
formfeed 改ページ
space 空白
tab 水平タブ
uniWhite 空白として定義されたUnicode文字
comment dashes [ anysymbol {any} ] newline
dashes -- {-}
opencom {-
closecom -}
ncomment opencom ANY seq {ncomment ANY seq} closecom
ANY seq {ANY}⟨{ANY} ( opencom | closecom ) {ANY}⟩
ANY graphic | whitechar
any graphic | space | tab
graphic small | large | symbol | digit | special | " | '
small ascSmall | uniSmall | _
ascSmall a | b | … | z
uniSmall 小文字Unicode
large ascLarge | uniLarge
ascLarge A | B | … | Z
uniLarge 任意の大文字またはタイトルケース(訳注: 先頭のみ大文字で後は小文字にするスタイル)のユニコード文字
symbol ascSymbol | uniSymbolspecial | _ | " | '
ascSymbol ! | # | $ | % | & | | + | . | / | < | = | > | ? | @ | \ | ^ | | | - | ~ | :
uniSymbol Unicodeのシンボル、または句読点
digit ascDigit | uniDigit
ascDigit 0 | 1 | … | 9
uniDigit 10進数Unicode
octit 0 | 1 | … | 7
hexit digit | A | … | F | a | … | f

字句解析は"maximal munch"規則に従うべきである。すなわち、語彙素生成規則を満たす可能な限り長くとった語彙素が読み取られる([訳注]:日本語訳、最長一致。"longest match" ともいう)。したがって、caseは予約語だが、casesは予約語ではない。同様に=は予約されているが、==~=は予約されていない。

空白はどんなものでも正しい字句の区切り文字である。

Any カテゴリではない文字はHaskellプログラム内では有効ではなく、字句解析エラーを結果にすべきである。

コメント

コメントは有効な空白である。

普通のコメントは二つ以上の連続したダッシュ(--など)で始まり、次の改行まで及ぶ。連続したダッシュは正当な語彙素の一部を形成してはいけない。例として、"-->"や"|--"はコメントの開始としては見なさない。なぜならこれらは正当な語彙素だからだ。しかしながら、"--foo"はコメントの開始として見なされる。

ネストされたコメントは"{-"で始まり、"-}"で終わる。"{-"は違法な語彙素ではない。それゆえ、例えば"{---"は末尾に余分なダッシュがあるがネストされたコメントの始まりである。

コメントそれ自体は語彙的に解析されない。代わりに、初めに文字列"-}"が現れた前の部分までがネストされたコメントの範囲である。ネストされたコメントは任意の深さにネストできる。ネストされたコメント内に文字列"{-"があると新しいネストされたコメントが始まり、"-}"によって閉じられる。ネストされたコメント内では、各"{-"は対応する"-}"の出現によって照合される。

普通のコメント内では"{-""-}"の文字の並びは特別な意味を持たず、一方でネストされたコメント内ではダッシュの並びは特別な意味を持たない。

ネストされたコメントはコンパイラープラグマのためにも使われる。それについては12章で説明される。

もし、いくつかのコードがネストされたコメントによってコメントアウトされていたら、その時、そのコード内の文字列内または行末コメントに"{-""-}"があるとネストされたコメントに干渉する。

識別子と演算子

varid (small {small | large | digit | ' })⟨reservedid⟩
conid large {small | large | digit | ' }
reservedid case | class | data | default | deriving | do | else
| foreign | if | import | in | infix | infixl
| infixr | instance | let | module | newtype | of
| then | type | where | _

識別子は0個以上の文字、数字、アンダースコア、およびシングルクォートで構成される。識別子は字句的に小文字で始まる字句(変数識別子)と大文字から始まる字句(コンストラクタ識別子)の二つの名前空間に区別される。(セクション1.4)これらの識別子は大文字と小文字を区別する。namenaMeNameは3つの判然たる識別子である。(初め2つは変数識別子で、最後のはコンストラクタ識別子である。)

アンダースコア("_")は小文字として扱われ、小文字が許されるところならどこでも使用可能だ。しかしながら、全て"_"なものはパターンのワイルドカードのように使われる識別子として予約されている。未使用の識別子に対して警告を出すコンパイラーはアンダースコアで始まる識別子に対しては警告を抑制することが推奨される。これはプログラマーが未使用であると予想されるパラメータに"_foo"を使うことを許可している。

varsym ( symbol⟨:⟩ {symbol} )reservedop | dashes
consym ( : {symbol})reservedop
reservedop> .. | : | :: | = | \ | | | <- | -> | @ | ~ | =>

演算子シンボルは上で定義したように、1つ以上の記号文字から形成され、2つの名前空間に字句的に区別される。(セクション1.4)

  • コロンから始まる演算子シンボルはコンストラクタである。
  • 他の文字から始まる演算子シンボルは普通の識別子である。

コロン(":")はHaskellリストのコンストラクタとして使用されるためだけに予約されている。これにより、"[]""[a,b]"のようなリスト構文の他の部分との扱いが統一される。

接頭辞否定の特殊な構文を除き、全ての演算子は中置である。ただし、各中置演算子をセクション内で使用して、部分的に適応される演算子を生成することができる。(セクション3.5を参照)標準の中置演算子はすべて定義済みのシンボルであり、リバウンドすることがある。

レポートの残りの部分では、6種類の名前が使用される。

varid (variables)
conid (constructors)
tyvar varid (type variables)
tycon conid (type constructors)
tycls conid (type classes)
modid {conid .} conid (modules)

変数と型変数は小文字で始まる識別子によって表され、そのほかは大文字で始まる識別子によって表される。また、変数とコンストラクタには中置形式があるが、他の4つにはない。モジュール名はドットで区切られた一連のconidである。名前空間についてはセクション1.4でも説明している。

特定の状況では、名前の前にモジュール識別子を付けることで、名前をオプションで修飾できる。これは変数、コンストラクタ、型コンストラクタ、型クラス、型クラス名に適応されるが、型変数やモジュール名には適応されない。修飾名については、チャプター5で詳しく説明する。

qvarid [modid .] varid
qconid [modid .] conid
qtycon [modid .] tycon
qtycls [modid .] tycls
qvarsym [modid .] varsym
qconsym [modid .] consym

修飾名は語彙素なので、修飾子と名前の間には空白を入れることはできない。サンプルの語彙解析を以下に示す。

これは このような語彙
f.g f . g (3トークン)
F.g F.g (修飾された'g')
f.. f .. (2トークン)
F.. F.. (修飾された'.')
F. F . (2トークン)

修飾子は名前の構文上の扱いを変更しない。例えば、Prelude.+はPrelude(セクション4.4.2)での+の定義と同じ固定性を持つ中置演算子である。

数値リテラル

decimal digit{digit}
octal octit{octit}
hexadecimal hexit{hexit}
integer decimal
| 0o octal | 0O octal
| 0x hexadecimal | 0X hexadecimal
float decimal . decimal [exponent]
| decimal exponent
exponent (e | E) [+ | -] decimal

数値リテラルには整数と浮動小数点の2種類ある。整数リテラルは10進数(デフォルト)、8進数(0oまたは0Oを先頭に付ける)、16進数表記(0x0Xを先頭に付ける)で指定できる。浮動小数点リテラルは常に10進数である。浮動小数点リテラルは小数点の前後に数値を含まなければいけない。これは小数点が他のドット文字の使い方に誤って認識されないことを保証するためだ。負数リテラルはセクション3.4で論じられる。数値リテラルの型はセクション6.4.1で論じられる。

文字と文字列リテラル

char ' (graphic⟨' | \⟩ | space | escape⟨\&⟩) '
string " {graphic⟨" | \⟩ | space | escape | gap} "
escape \ ( charesc | ascii | decimal | o octal | x hexadecimal )
charesc a | b | f | n | r | t | v | \ | " | ' | &
ascii ^cntrl | NUL | SOH | STX | ETX | EOT | ENQ | ACK
| BEL | BS | HT | LF | VT | FF | CR | SO | SI | DLE
| DC1 | DC2 | DC3 | DC4 | NAK | SYN | ETB | CAN
| EM | SUB | ESC | FS | GS | RS | US | SP | DEL
cntrl ascLarge | @ | [ | \ | ] | ^ | _
gap \ whitechar {whitechar} \

文字リテラルは'a'のようにシングルクォーテーションで囲まれたものであり、文字列リテラルは"Hello"のようにダブルクォーテーションで囲まれたものである。

エスケープコードは特殊文字の表現のために文字と文字列で使われる。注意すべきことはシングルクォーテーション(')は文字列においても使われるが、文字でエスケープする必要があることだ。同様に、ダブルクォーテーションは文字の中で使われるが、その際は文字列でエスケープする必要がある。\は常にエスケープしないといけない。charescカテゴリには"アラート"(\a)、"バックスペース"(\b)、"改ページ"(\f)、"改行"(\n)、"キャリッジ・リターン"(\r)、"水平タブ"(\t)、"垂直タブ"(\v)といった文字のポータブル表現も含まれている。

\^Xのような制御文字を含むUnicode文字セットのエスケープ文字も用意している。\137のような数値エスケープは10進数表現の137で文字を指定するために使用され、同様に8進数(例:\o137)や16進数(例:\x37)の表現も可能である。

“maximal munch”に従って、文字列内の数字のエスケープ文字は全て連続した数字で構成され、任意の長さにすることができる。同様に、"\SOH"のような奇妙なASCIIエスケープコードは長さ1の文字列としてパ-スされる。'\&'エスケープ文字は"\137\&9""SO\&H"のような文字列(共に長さは2)が構成できるように"ヌル文字"として提供される。その代わり"\&"""に等しく、'\&'文字は許されない。文字の等価性はセクション6.1.2で定義されている。

文字列は無視される"ギャップ"(白い文字を囲む2つのバックスラント)を含むかもしれない。これにより1行の終わりと次の行の始めにバックスラントを書くことによって、複数の行に長い文字列を書くことが可能だ。例としては以下のものになる。

"Here is a backslant \\ as well as \137, \  
    \a numeric escape character, and \^X, a control character."

文字列リテラルは実際には文字のリストの略記である。(セクション3.7を参照)

レイアウト

Haskellはレイアウトを使用して同じ情報を伝えることによって、いくつかの文法で使用されている中括弧とセミコロンの省略を許可している。これによりレイアウトに依存するもの、しないものの両方のコーディングスタイルが可能になり、1つのプログラム内で自由に混在させることができる。レイアウトが必要ではないため、Haskellプログラムは他のプログラムによって簡単に作成することができる。

レイアウトがHaskellプログラムの意味に与える影響は、レイアウトによって決定される場所に中括弧とセミコロンを追加することによって完全に指定できる。この拡張プログラムの意味はレイアウトに影響されなくなった。

非公式には中括弧とセミコロンは次のように挿入される。キーワードのwhereletdo、またはofの後に開き括弧が省略されると、レイアウト(または"オフサイド")規則が有効になる。これが起こると、次の語彙素のインデントが(新しい行にあるかどうかにかかわらず)記録され、省略された開き括弧が挿入される(語彙素の前の空白にはコメントが含まれる場合がある)。後続の行について、空白のみが含まれている場合、またはそれ以上のインデントされている場合は、前の項目が続行される。(何も挿入されない)同じ量だけインデントされている場合は新しい項目が始まる(セミコロンが挿入される)。またインデントが小さくなると、レイアウトリストは終了する(閉じ括弧が挿入される)。whereletdoまたはofの直後のノンブレース語彙素のインデントが現在のインデントレベル以下である場合は、レイアウトを開始する代わりに、空のリスト"{}"が挿入され、現在のレベルでレイアウト処理が発生する(つまり、セミコロンまたは閉じ括弧を挿入する)。レイアウトリストを含む構文カテゴリが終了するたびに、閉じ括弧も挿入される。つまり、閉じ括弧が合法となる点で違法な語彙素が検出された場合は閉じ括弧が挿入される。レイアウトルールはそれが挿入した開いている中括弧にだけ一致する。明示的な開き括弧は、明示的に閉じ括弧と一致しなければならない。これらの明示的な開き括弧内では、たとえ行が以前の暗黙の開き括弧の左側に字下げされていても、括弧の外側の構成要素に対してレイアウト処理は実行されない。

セクション10.3ではレイアウトルールのより正確な定義を示す。

これらの規則を考えると、1つの改行で実際に複数のレイアウトリストを終了させることができる。 これらの規則は以下のコードを許す。

f x = let a = 1; b = 2  
          g y = exp2  
       in exp1

生成したa, b, gは全て同じレイアウトリストの一部である。

例として、図2.1は(ややわざとらしい)モジュールを示し、図2.2はそのレイアウトルールを適応した結果を示している。次の部分に注意: (a) }};popで行が開始している個所において、前の行が終了すると、ネストしたwhere区の深さ(3)に対応する3つレイアウトルールの利用が呼び出される。(b)where句の閉じ括弧はタプルとcase式にネストされており、タプルの終了を検出されたため挿入された。(c)一番最後の閉じ括弧は、Eofトークンの0列のインデントにより挿入された。

module AStack( Stack, push, pop, top, size ) where  
data Stack a = Empty  
             | MkStack a (Stack a)  

push :: a -> Stack a -> Stack a  
push x s = MkStack x s  

size :: Stack a -> Int  
size s = length (stkToLst s)  where  
           stkToLst  Empty         = []  
           stkToLst (MkStack x s)  = x:xs where xs = stkToLst s  

pop :: Stack a -> (a, Stack a)  
pop (MkStack x s)  
  = (x, case s of r -> i r where i x = x) -- (pop Empty) is an error  

top :: Stack a -> a  
top (MkStack x s) = x                     -- (top Empty) is an error

図2.1: サンプルプログラム

module AStack( Stack, push, pop, top, size ) where  
{data Stack a = Empty  
             | MkStack a (Stack a)  

;push :: a -> Stack a -> Stack a  
;push x s = MkStack x s  

;size :: Stack a -> Int  
;size s = length (stkToLst s)  where  
           {stkToLst  Empty         = []  
           ;stkToLst (MkStack x s)  = x:xs where {xs = stkToLst s  

}};pop :: Stack a -> (a, Stack a)  
;pop (MkStack x s)  
  = (x, case s of {r -> i r where {i x = x}}) -- (pop Empty) is an error  

;top :: Stack a -> a  
;top (MkStack x s) = x                        -- (top Empty) is an error  
}

図2.2: レイアウトを展開したサンプルプログラム