Blog 2023‐04‐02 - qnighy/umo GitHub Wiki
昨日の進捗はない。 (ipc_botのMastodon対応を行っていた……)
プログラミング言語の具象文法を設計する上で重大な判断のひとつは、括弧資源の配分である。コンピューターの文字処理において、使いやすい括弧は以下の4つしかない:
- 丸括弧 parentheses
(
)
- 角括弧 brackets
[
]
- 波括弧 braces
{
}
- 山括弧 angle brackets
<
>
-- ただし山括弧は不等号との曖昧性がある
S式に見られるような思い切った割り切りをしない限り、これらの括弧はすぐに需要を見出されてしまい、あとから発生した文法は残った空間をせせこましくやりくりするしかなくなることが多い。
そこで今回は既存言語における括弧の用法を整理し、トレードオフを考慮しつつもなるべく合理的な括弧の割り当てについて考えてみたい。
プログラミング言語の構文は記号識別の観点からは大きく以下の2種類に分けられる。
- 生成列に
Expr Expr
が出現しうる言語 -- OCaml, Haskell, Scheme, Rubyなど - 生成列に
Expr Expr
が出現しえない言語 -- C++, Java, JavaScript, Rust, Pythonなど
Expr Expr
は通常、関数適用規則 Expr -> Expr Expr
に由来する。これについては並置による関数適用についての所感でも整理したが、ここでもあらためて説明する。
1に類する言語では、 FIRST(Expr) ⊆ FOLLOW(Expr) が要請される[^first-follow]ため、原則として前置記号は中置記号と共存できない。この問題を迂回して前置記号と中置記号を共存させる戦略としてはたとえば以下のようなものがある:
[^first-follow]: FIRST, FOLLOWについてはLL grammar - Wikipedia等を参照。ここではFIRST, FOLLOWをFi, Foと呼んでいる。
- Haskellではセクションと単項マイナスのために、括弧式の冒頭などの限られた箇所で前置演算子を許可している。これらの位置ではExprが先行しないため、関数適用との曖昧性はない。
- Rubyでは字句解析器に複雑なロジックが詰め込まれており、同じ見た目の記号であってもスペースやローカル変数の有無などのいくつかの条件によって異なるトークンが出力されるようになっている。これにより
f -x
とf - x
で異なる解釈が行われたりする。
一方、2に類する言語では、ひとつの記号に前置と中置で異なる意味を付与できる。たとえばC言語であれば a * b
と *p
で *
は異なる役割を担っている。これは演算子に限った話ではなく、括弧に関しても式の頭で出現した場合と式の後続で出現した場合で異なる役割を与えることができる。
ここでは各言語の主要構文 (Expr, Stmt, Decl, Pat, Ident などと呼ばれているもの) をまとめて扱う。また、異なる生成規則であっても、ユーザーの意味理解が近いものは同一視することにする。たとえば、式のグルーピングとパターンのグルーピングは同じものとして扱う。
( ) 前置 |
中置 |
[ ] 前置 |
中置 |
{ } 前置 |
中置 |
< > 前置 |
中置 |
|
---|---|---|---|---|---|---|---|---|
C++ | group seq |
call | closure | index | array record block |
block | ? | lt generics |
Java | group | call | - | index | array block |
array block |
generics | lt generics |
TypeScript | group seq closure |
call | array | index | record block |
block | tag generics |
lt generics |
Ruby |
凡例
- group -- 式・パターンの意味を変えずにまとめる
(1 + 2) * 3
- block -- 複数の文をまとめる
{ f(); g(); }
- seq -- 複数の式を連続して実行する
(f(), g())
- closure -- クロージャ・ラムダ式・無名関数などと呼ばれるもの
(x) => x + 1
[](int x) { return x + 1; }
- call -- 関数呼び出し
f(x)
。- 関数呼び出し形式で宣言される関数宣言も含む
function f(x)
- 関数呼び出し形式で宣言される関数宣言も含む
- index -- 配列などの複合値から特定の要素を取り出す演算
a[i]
- array -- 配列
[1, 2, 3]
- tuple -- 配列風のリテラルだが、丸括弧で初期化されheterogeneousな意味論を持つもの
("foo", 1, true)
- record -- 名前と値の組み合わせで初期化される複合リテラル
{ foo: 1, bar: 2 }
- generics -- 型パラメーターを指定するための括弧
f<number>(42)
- lt -- 比較演算子
x < y
- tag -- XML/HTML風のタグの開始
<div></div>