なぜコードは曖昧ではいけないのか?プログラミング言語が数学で作られる理由
私たちが日常的に使うプログラミング言語。その一見シンプルな文法の裏には、数十年にわたる数学理論の蓄積があります。なぜプログラミング言語は厳密なルールを持ち、曖昧さを許さないのでしょうか。本稿では、形式言語理論という数学的基盤から、現代のプログラミング言語がどのように設計されているかを解き明かします。
Spotifyでわかりやすく音声配信:「らみのAIテックラジオ」
世界経済フォーラムの調査では「2030年に必要なスキル」の第5位に「好奇心と生涯学習」がランクイン。好奇心は趣味ではなく経済的必須スキル。
拙著『AI時代の最強スキル「好奇心力」』で詳説しています。
まえがき
プログラミングを学び始めたとき、「なぜセミコロンを忘れただけでエラーになるのか」「なぜ括弧の位置一つで動作が変わるのか」と疑問に思ったことはないでしょうか。
実は、この厳密さにはコンピュータサイエンスの深い理論的背景があります。プログラミング言語は、人間の思考をコンピュータが理解できる形に変換する橋渡し役。その設計には、曖昧さを完全に排除し、誰もが同じように解釈できる仕組みが不可欠なのです。
言語における「曖昧さ」とは何か?
プログラミング言語の設計思想を理解する上で、最初のステップは「曖昧性」という概念を正確に捉えることです。人間が使う自然言語と、機械が解釈する人工言語(プログラミング言語)では、この曖昧性の扱いが根本的に異なります。
自然言語に潜む解釈の多様性
私たちの日常会話は、意識せずとも高度な文脈依存処理の上に成り立っています。例えば、以下のような文を考えてみましょう。
「大きな子供用の自転車」
この文には、少なくとも二つの解釈が存在します。
(大きな子供)用の自転車:体が大きい子供が乗るための自転車
大きな(子供用の自転車):子供用自転車の中でも、特にサイズの大きいもの
このような解釈の揺れを構造的曖昧性と呼びます。一つの文が、複数の構造(構文木)を持ちうる状態を指します。自然言語において、私たちは話の流れや常識、相手の表情といった文脈情報を使って、無意識のうちに最も確からしい解釈を選択しています。
しかし、コンピュータにはそのような「常識」や「空気」を読む能力はありません。もしプログラミング言語にこのような曖昧性が存在したら、一つのコードが開発者の意図とは異なる複数の動作を引き起こす可能性が生まれてしまいます。
プログラミング言語設計の目標は、いかなる構文的にも正しいコードも、常に単一の意味しか持たないようにすることです。つまり、構文的にも意味的にも曖昧性を排除することが、言語設計における至上命題なのです。この大原則を達成するために、コンピュータサイエンスの研究者たちは、数学の一分野である「形式言語理論」にその理論的支柱を求めました。
形式言語学の基本概念
形式言語理論は、言語の構文や意味を数学的な定義に基づいて厳密に扱うための理論です。人間の自然言語(日本語や英語など)は多義性や曖昧さが大きく、形式的に厳密な定義が難しいですが、プログラミング言語のような人工言語では形式文法によって言語を定義することで理論的基盤が築かれ、曖昧さのない言語設計が可能になります。
形式文法では、言語を構成する基本要素(文字や記号の集合であるアルファベット)、アルファベットからなる文字列(語)、そしてどの文字列が「文法的に正しい文」になるかを決める生成規則の集まりとして言語を定義します。
構文とは、ある記号列(プログラムのコードなど)が言語として正しい形式を満たしているかを決定する規則の集合のことです。プログラミング言語では、構文上正しいコードでなければコンパイル時に「構文エラー」として弾かれます。
一方、意味論は構文的に正しいプログラムに対して、その実行結果や効果といった「意味」を与える規則や解釈の体系です。
プログラムの意味を厳密に定義することは構文以上に難しく、特に命令型言語では数学的に明確な基盤がないため、形式的意味論としては操作的意味論(簡易な計算モデル上でプログラムの動作を定義する)や公理的意味論(ホーアの論理によってプログラムの仕様と正当性を記述する)、「表示的(デノテーション的)意味論」(プログラムを数学的対象に対応付ける)などの手法が研究されてきました。
関数型言語ではラムダ計算、論理型言語では一階述語論理といった理論を基盤に意味を定義できますが、いずれの場合もプログラミング言語の各構成要素について単一の意味を持つよう慎重に設計・定義されます。つまり、プログラミング言語では構文的にも意味的にも曖昧性の排除が重要です。
構造的曖昧性とは、一つの文(コード断片)に対して複数の解釈または構文構造(構文木)が存在してしまう性質を指します。形式文法の観点では、ある文法が曖昧であるとは「一つの文字列に対して二通り以上の構文解析(導出経路や構文木)が可能である」ことを意味し、逆にどんな文に対しても常に唯一の構文木しか得られない文法を非曖昧と呼びます。
通常、コンパイラなどの言語処理系では非曖昧な文法が用いられ、曖昧性のある文法は扱いません。プログラミング言語においては人間の解釈違いが生じないよう、この文法レベルでの曖昧性を原則として完全に排除することが設計上の大前提となっています。
例えば自然言語では「大きな黒い犬」という表現が「犬が大きい」「尻尾が大きい」など複数の構造解釈を許しますが、プログラミング言語の文法では同じコードが複数の意味に取れることがないよう厳密に規定されます。
プログラミング言語設計への理論の応用
プログラミング言語はその構文規則に数学的な形式言語理論を活用しており、曖昧性のない厳密な構文を持つよう設計されています。具体的には、プログラミング言語の字句構造から文法構造までを段階的に形式モデルで定義します。
まず字句解析では、プログラムの文字列をトークン(識別子、キーワード、リテラル、記号など)という最小単位に分割しますが、このトークンのパターンは正規表現などの正則言語(タイプ3文法)で記述できます。実際、多くの言語で識別子の定義やリテラルの形式は正規表現で表され、オートマトン(有限状態機械)によって字句解析器が実装されています。
次に構文解析では、得られたトークン列が言語の文法規則(多くは文脈自由文法(タイプ2文法)で記述される)に従っているかを解析し、構文木を生成します。ほとんどのプログラミング言語の文法は、この文脈自由文法で表現可能です。
文脈自由文法とは、各生成規則の左辺が単一の非終端記号であるような文法で、規則適用において周囲の文脈に制約を受けない形式です。文脈自由文法に基づく言語はプッシュダウン・オートマトン(スタックを用いたオートマトン)で認識できることが知られており、効率的なパーサ(構文解析器)の実装が可能です。
一般にコンパイラでは、構文解析には再帰下降法(トップダウン解析)やLR法(ボトムアップ解析)などのアルゴリズムが使われますが、いずれも線形時間O(n)で解析できる文法クラス(LL(1)文法やLALR(1)文法など)の範囲内で言語文法が設計されます。
実際、任意の文脈自由文法に対しては最悪O(n^3)の一般アルゴリズム(CYK法など)も存在しますが、それでは実用的なプログラム(何万行ものソースコード)を解析するのに時間がかかりすぎるため、プログラミング言語の文法自体が線形時間で解析可能となるよう最適化・制限して設計されているのです。
さらに構文解析の後には意味解析が行われます。意味解析では、構文木あるいは抽象構文木に対して型の整合性チェックや宣言・参照の対応付け、定数畳み込みなどを行い、プログラムの意味的な正しさを検証します。
これらのチェックは形式言語理論というよりは論理学や型システムの理論に基づいており、場合によっては文脈依存の条件になります。
例えば変数が宣言されてから使われているか、型変換が安全か、といった条件は文脈自由文法だけでは記述できないため、コンパイラは構文木と補助的な情報(シンボル表など)を用いてこれら静的セマンティクスの規則を適用します。
このように、プログラミング言語の設計では字句レベルでは正則文法、構文レベルでは文脈自由文法、意味レベルでは必要に応じて文脈依存の制約や論理体系を用いることで、それぞれの段階に適した形式理論を活用しています。
チョムスキー階層とプログラミング言語
言語理論では、ノーム・チョムスキーによって形式文法が生成力の違いに応じて4つのクラスに分類されており、これをチョムスキー階層と呼びます。下位ほど表現力が弱い代わりに解析が容易で、上位ほど強力な文法クラスになります。それぞれの文法クラスとプログラミング言語設計との関係を整理すると次の通りです。
タイプ0文法(無制限文法)
生成規則に一切の制限がなく、チューリング機械で受理可能なあらゆる言語(計算可能な言語)を記述できます。しかし解析問題は一般に決定不能であり、現実の言語仕様に直接使われることはありません。プログラミング言語の定義としてもタイプ0の文法が使われることはまずなく、より制限された下位クラスで十分かつ効率的に記述できます。
タイプ1文法(文脈依存文法)
各生成規則で「左辺の長さ ≤ 右辺の長さ」という制約がある文法で、線形有界オートマトンで認識可能な言語クラスです。左辺の非終端記号の周囲(文脈)によって適用可能な生成規則が決まるため文脈「依存」と呼ばれます。
表現力は高いものの解析には時間がかかり、最悪で入力長の指数関数的時間を要することもあります。プログラミング言語レベルでは、文脈依存文法そのものが構文定義に使われることは稀です。
ただし一部、後述するように型や名前解決など文脈に依存する要素を扱う際に事実上タイプ1的な処理が必要となるケースがあり、それらは文法ではなく意味解析段階で対応します。
タイプ2文法(文脈自由文法, CFG)
各生成規則の左辺が単一の非終端記号から成る文法で、スタックを用いたプッシュダウン・オートマトンで認識可能な言語クラスです。ほとんど全てのプログラミング言語の構文定義には文脈自由文法が採用されています。
文脈自由文法は表現力と解析効率のバランスが良く、前述の通りLL法やLR法など線形時間の構文解析アルゴリズムが適用可能です。実際には文脈自由文法であっても、そのままでは曖昧だったり解析が困難な場合があるため、後述する設計原則に従ってさらに制限された文法(LL(1)文法やLALR(1)文法等)に落とし込んで用いることが一般的です。
タイプ3文法(正規文法, Regular Grammar)
各生成規則が「非終端 → 終端記号 又は 終端+非終端」という形のみから成る最も制限された文法で、これは正規言語(正則言語)を生成します。正規言語は有限オートマトンで受理可能な言語であり、解析は非常に高速(線形時間かつ定数時間のループ処理)で実装できます。
プログラミング言語分野では主にトークン列の定義(字句文法)に正規文法が使われます。例えばC言語で識別子は「アルファベットと数字から成り、先頭は数字不可」という正規表現で定義され、リテラルも数字や文字のパターンとして正規表現で定義されます。
このように字句レベルでは正規文法で十分であり、実際ほとんどのコンパイラがレクサ(lexer)と呼ばれる有限オートマトンベースのモジュールでトークン分割を行っています。
以上のように、プログラミング言語設計ではチョムスキー階層の下位に属する文法理論(タイプ2およびタイプ3)を組み合わせて用いるのが一般的です。
強力だが解析困難なタイプ0やタイプ1の機能は極力使わず、必要な場合も構文ではなく別フェーズで扱うことで、言語処理系全体の効率と実装容易性を確保しています。
例えばPythonのインデントによるブロック構造は一見文脈依存ですが、字句解析時にインデントの増減を特殊トークン(INDENT/DEDENT)に置き換えることで文脈自由文法として扱っています。これも階層上位の要素(レイアウトによる構造)を下位の仕組み(トークン化)で処理する工夫の一例と言えます。
構文解析(パーサ)、意味解析、抽象構文木(AST)などの形式的手法
プログラミング言語のコンパイラやインタプリタは、入力されたソースコードを内部表現に変換し実行・解析する過程で、さまざまな形式的手法を用いています。構文解析 はその中心的な工程で、文脈自由文法で定義された言語の規則に従い、トークン列から構文木を構築します。
構文木は、入力文がどのような生成規則の適用によって得られるかを木構造で示したもので、根から葉までの階層がコードの構造(式や文の入れ子関係)を表現します。例えば式 A*(B+C) を与えると、図1のように乗算と加算の優先関係を反映した構文木が得られます(非終端記号<expr>や<term>は文法上の構成要素を示す)。

左の構文木 (①) では先に乗算(*)を評価してから加算(+)を行っている。一方、右の構文木 (②) では先に加算を行ってから乗算を行っており、この文では二通りの解析が可能であるため文法に曖昧性が生じている。
このような曖昧な文法では、どちらの構文木が正しい解釈か文法だけでは決められないため不都合です。多くのプログラミング言語では、この例のような演算子の優先順位と結合規則を文法に組み込むことで曖昧性を解消しています(後述の設計原則の節を参照)。
構文木は厳密には文法の生成規則に沿った具象構文木ですが、コンパイラ内部ではこれを簡略化した抽象構文木が使われます。抽象構文木とは、通常の構文木から言語の意味に関係ない余分な情報(例えば括弧やセミコロンといった区切り記号、キーワードの一部など)を取り除き、意味に直接関係する構成要素(構文構造と演算子・オペランドなど)のみを残した木構造です。
ASTでは括弧そのものは表示されず、木の階層構造自体が暗黙に演算子の優先順位やブロック構造を表します。プログラミング言語処理系では、構文解析によってまず具象構文木が得られ、これを中間表現の一種であるASTに変換して以降の処理(最適化やコード生成)を行います。
例えばコンパイラの最適化ではAST上で不要な計算の削除や式の変形が行われ、インタプリタではASTを直接走査して実行することもあります。ASTは言語の抽象的な文法構造を表しており、形式的にはAST自体の構造を定義する抽象文法(抽象構文)という考え方もあります。
総じて、ASTによってプログラムの構造をデータ構造化することで、コンピュータにとって扱いやすくし、意味解析や最適化などの形式的処理を施しやすくしているのです。
構文解析に用いられるアルゴリズムには、主にトップダウン型(再帰下降パーサなど)とボトムアップ型(LRパーサなど)の二系統があります。トップダウンパーサは文法の開始記号から再帰的に生成規則を適用して木を構築し、入力を先読みしながら予測的に解析します。
一方、ボトムアップパーサは入力トークン列から部分構造を徐々に積み上げて(シフト・リデュース操作により)構文木を組み立てます。双方とも一般的にはバックトラック(解析の行き詰まり時の巻き戻し)無しで動作するよう工夫された文法(LL(1)やLALR(1)など)を前提としており、各トークンを一度ずつ読み左から右へ線形時間で解析します。
トップダウン方式では左再帰を文法から除去しないと無限再帰に陥るため、文法の書き換え(左再帰を右再帰に変換)などの前処理が施されます。
ボトムアップ方式では自動的にパーサ生成が可能ですが、文法に二義的解釈(シフト/リデュースの競合)がある場合には生成時に警告が出るため、言語設計者はそのような競合が起きない文法になるよう調整します。
いずれにせよ、言語の文法設計と構文解析手法は表裏一体であり、形式言語理論に基づく文法設計があるからこそ機械的な構文解析プログラムの実現が可能になっています。
最後に意味解析ですが、これは構文木をもとにプログラムの静的意味情報を解釈・検証する工程です。具体的には型チェック(各式が期待された型に適合するか)、型推論、変数の宣言と参照の照合(スコープと名前解決)、定数の畳み込み、制御構造の到達不能コード検出など、多岐にわたるチェックが行われます。
意味解析の多くはコンパイル時に完了し、問題があればエラーメッセージを報告します。これらの意味解析のルールは、形式的には型システムや論理体系として定義でき、論文などでは推論規則(ジャッジメントと導出)という形で形式的に示されます。
またホーア論理に基づくアサーションチェックやモデル検査など、形式意味論の枠組みでプログラムの性質を検証する技術もあります。ただし一般的な言語処理系では、意味解析は形式的仕様というより実装寄りの記述(擬似コードや自然言語の仕様書)で定義されることも多いです。
それでもプログラミング言語の設計者は、各プログラムが単一の明確な意味を持つよう慎重に仕様を定めます。例えば「未初期化変数の値読み出しは未定義」とすることで意味の揺れを許さない、演算のオーバーフローは指定の動作をすると決める、など言語の意味論を明文化し、曖昧さを残さないようにします。
総じて、構文解析から意味解析に至る各段階で、形式言語理論や計算機科学の理論を応用することで、プログラミング言語はコンピュータが機械的に処理できる厳密さと一貫した解釈(意味の一意性)を獲得しているのです。
曖昧性を排除するための設計原則や記述法(BNF、EBNFなど)
プログラミング言語の文法を設計・記述する際には、曖昧性を排除するためにいくつかの原則や記法上の工夫が用いられます。最も基本的なものがBNF(Backus-Naur Form)とその拡張であるEBNF(Extended BNF)です。
BNFはアルゴル60言語の報告書で、ジョン・バッカスとピーター・ナウアによって考案された文法記法であり、プログラミング言語の構文を厳密に定義するためのメタ言語です。
BNFでは、語彙記号(終端記号)と非終端記号、生成規則を用いて文脈自由文法を記述します。記法上、終端記号はそのまま文字や文字列で記述し、非終端記号は例えば<statement>のように山括弧で囲んで表します。
生成規則は「<非終端> ::= 生成パターン」の形で書き、|で区切って複数の選択肢を並べることで「いずれか」を表現します。BNFの導入により、それまで曖昧さを残しがちだった言語仕様を形式的に記述できるようになり、以降の多くの言語で公式の文法定義に採用されました。
例えばALGOLやPascal、C言語の標準規格書、Java言語仕様書などは、言語文法をBNFあるいはEBNFで明示的に列挙しています。
EBNFはBNFをより実用的に拡張した記法の総称です。BNFでは再帰によってしか表現できなかった「繰り返し」や「省略可能」といった要素を、EBNFでは記号を用いて簡潔に書けます。
例えばEBNFの一例では、...*や{ ... }で「0回以上の繰り返し」、+で「1回以上の繰り返し」、[ ... ]で「省略可能な要素」を表すことができます。実際の文法定義では、演算子の優先順位規則などを表現する際に再帰を用いる代わりに、このような繰り返し記号を使った方が簡潔で分かりやすい場合があります。
また、BNFではコメントや空白文字の扱いが明示されませんが、多くのEBNFではコメント文法や空文字(ε)の表現法も規定されています。EBNFは正式な単一仕様があるわけではなく、言語仕様書ごとに若干記号が異なる場合もありますが、総じてBNFで書けるものはEBNFでも書け、EBNFを使えば文法定義がより読みやすくなるため現在では主流の記法となっています。
BNF/EBNFによって文法を明確に記述すること自体が、曖昧性の発見と排除に役立ちます。文法を定義した後、その文法が曖昧かどうかはしばしば注意深いレビューやツールによる検査で判定されます。典型的な曖昧性の例として前述した演算子の優先順位問題があります。
図1に示したように、+と*のような演算子を同じレベルで文法に含めると曖昧性が生じますが、これを避けるには演算子ごとに別の非終端記号レベル(加法項と乗法項など)を導入し、文法上の階層を分けます。
実際、ほとんどの言語の式文法は「項 <term>」「式 <expr>」のように階層的な非終端を使い、加減算と乗除算を別の生成規則系列で定義することで、一義的な構文解析が可能になっています。
また括弧は曖昧性解消の強力な手段で、文法上で曖昧になりうる結合は利用者に括弧で明示させるか、文法に組み込んで括弧付きの構文ルールを用意します。例えば算術式で優先順位が混在する場合、利用者が任意に(と)でグルーピングできるよう文法にF -> '(' E ')'のような規則(Factorは括弧で囲まれた式)を入れるのが普通です。括弧があればその内側は一つの単位(非終端記号<expr>に対応)として扱われ、曖昧な解釈は発生しません。
他にも曖昧性を排除する設計原則はいくつかあります。「最大の貪欲マッチ」や「キーワードの予約」もその一つです。たとえばFORTRANの昔の仕様ではキーワードと識別子の区別が曖昧で、DO 5 I = 1, 10が「DO文」か「代入文(変数DO5Iへの代入)」か解釈が揺れる問題がありました。
現代の言語ではキーワード(if, for, while等)をあらかじめ予約語として識別子と区別することで、字句上の曖昧さを無くしています。このように曖昧さが生じそうな場合は仕様レベルで禁止するのが設計原則です。
C言語ではprintfなどの関数名にキーワードと同じ名前を付けられないよう予約語を定義し、字句解析時点でトークンを区別します。同様に「文法の二義性を文脈で解決しない」という方針も重要です。
例えばdangling else問題(if文にelseが一つ余計についたとき、どのifに対応するか不明瞭になる問題)は、有名な構文上の曖昧性です。CやJavaではこの問題に対し、「単一のelseは直前(最も内側)のifに対応する」という取り決めをしていますが、これは厳密には文法外の規則です。
より形式的な解決策としては、二種類のif文の非終端記号(else付きifとelse無しif)を用意し文法を改良する方法があります。実際にAdaやModula-2などではendifキーワードで明示終了させることで曖昧性を根本から除去しています。このように、「曖昧な構文には必ず解決策を与える」ことが言語設計の鉄則です。
まとめると、BNF/EBNFで形式的に文法を記述し、その過程で曖昧性の有無を検証し、必要に応じて文法の階層化・追加の非終端記号・キーワード予約・区切り記号の導入といった措置を講じることで、プログラミング言語の曖昧性は排除されます。
プログラミング言語の設計者は言語仕様書の文法セクションに特別な注意を払い、どんなコードも一意に解釈できることを保証するのです。
代表的なプログラミング言語における文法仕様と形式理論
ここでは、いくつか代表的なプログラミング言語を例に、その文法仕様と背後にある形式的理論について概観します。
C言語
C言語の文法はK&Rの初期から、ANSI/ISO標準規格に至るまでBNF的記法で定義されています。C言語の構文自体は基本的に文脈自由文法で表現できますが、一部に文脈依存的な要素が存在することでも知られます。
その代表例がtypedefによる名前の解釈です。Cでは、ある識別子が型名(typedefで定義された)か変数名かによって後続するコードの構文上の解釈が変わる場合があります。例えばX Y;というコードが「変数Yの宣言」か「関数呼び出し文」かは、Xが型名かどうかで決まります。
このように文脈(シンボル表の内容)に依存して構文解析結果が変わるケースがあるため、C言語は「文脈に依存する言語」と呼ばれることがあります。対策として、コンパイラ実装ではレキサとパーサの協調によってこれを処理します。
一つの手法は「lexer hack」と呼ばれるもので、字句解析の段階で事前にtypedefされた識別子を特別なトークン(例:TYPENAME)に分類し、構文規則で型名を他の識別子と区別して扱う方法です。
Clangなどのモダンなコンパイラでは別の手法(まずtypedef宣言を記憶し、構文解析中にそれを照合して判断する)を取っていますが、いずれも文脈自由文法で表現しきれない部分を補う仕組みといえます。
この点を除けば、C言語の大部分の文法はLALR(1)文法として整理されており、yaccやbisonといったパーサジェネレータで自動生成可能です。実際、C99やC11規格書には付録に形式文法が掲載されており、式文法、文文法、宣言文法など細かくBNF形式で定義されています。
C++は更に複雑ですが、同様に規格で膨大なEBNF文法が示され、曖昧性が生じないよう細心の注意が払われています。
Java
Java言語は、その仕様書で言語文法を厳密に定義した好例です。Java言語仕様書では字句文法と構文文法の双方が形式的に与えられており、JLS第2章で「本仕様で用いる文法はコンテキストフリー文法である」と明言しています。
具体的には、Unicode文字を入力とする字句文法(3章に定義)によりトークン(識別子、リテラル、キーワードなど)の並びに変換し、そのトークン列を構文文法によってプログラム構造(文や式の階層)に解析します。
字句文法と構文文法はいずれもBNF/EBNF形式で記述され、例えば識別子は正規表現的に定義され、一方で構文文法では非終端記号IfThenStatementのようにそれぞれの構文要素が定義されています。
さらにJLSでは、読みやすさのための文法と実装しやすい文法の両方を提示しており、後者は前者を等価変換したものですが、LR(1)パーサで処理しやすい形に調整されています。例えばオプション要素は後ろにoptと添える記法で表し、それは実際には2通りの規則(ある場合とない場合)を表す省略記法だと説明されています。
このように、Javaの文法定義は非常に厳密かつ詳細で、公式のEBNFに則った定義のおかげでJavaプログラムの構文的曖昧さは存在しません。実行時の意味についても、JLSは自然言語による記述ながら細部まで規定しており、仕様上の未定義動作を極力なくす方針を取っています。これにより、同じJavaプログラムはどの実装でも同じ意味に解釈・実行されることが保証されています。
Python
動的型付け言語であるPythonも、形式的な文法定義を持っています。Pythonのリファレンスでは文法をEBNFとPEGの混合形式で記述しています。PEG(Parsing Expression Grammar)は文脈自由文法に似た表現力を持ちながら、「最初にマッチした規則を採用する」という決定的な解析戦略を備えた文法体系です。
Pythonはバージョン3.9から公式実装のパーサをPEGベースに変更しており、言語仕様もそれに合わせPEG的記法(先読み&や否定先読み!記号など)を含めています。PEGを用いることで、文法に曖昧性があっても定義上は「最初の規則が勝つ」ためパーサ実装上は問題になりません。
ただしPythonの文法自体、従来から優先順位や結合性が明確になるようきちんと階層化されており、BNF的に見ても非曖昧です。例えばPythonの算術演算子はexpr, term, factorといった非終端で段階付けされ、それぞれの生成規則が定義されています(乗算はtermレベル、加算はexprレベルという具合に)。
またPythonの字句ルールでは、先述のようにインデント(字下げ)をトークン化することでブロック構造を文法に組み込み、複文もセミコロンや改行によって明確に区切られるよう規定されています。
これらの工夫により、Pythonのコードも一意的に字句解析・構文解析可能であり、解釈の揺れはありません。さらにPythonでは公式にASTモジュールを提供しており、プログラムをAST形式に変換して解析・操作できるようにしています。これはPython言語の設計がASTを念頭に置いており、仕様として抽象構文を意識していることを示す例と言えるでしょう。
その他の言語
多くのプログラミング言語が、上記と同様のアプローチで形式的な文法定義を持ちます。たとえばPascalやAdaは教育用途・ミッションクリティカル用途を意識して、文法を厳格なLL(1)に制限し人間にも機械にも解析しやすい構造になっています。
Adaは特に曖昧さを嫌い、thenやend ifなどブロックの明示終端を採用し、dangling else問題も含めて曖昧解消されています(Ada文法はADA Reference ManualにEBNFで記載)。
JavaScriptは公式にはECMA-262仕様書で文法が示され(終端記号にUnicode正規名を使う独特のEBNFを採用)、やはりコンテキストフリーな文法に基づいています。
一方、Lisp系言語は表面上文法がシンプル(S式由来の構造)で曖昧性がなく、構文解析がトークン分割程度で済む特殊な例です。関数型言語のHaskellではレイアウト規則(オフサイドルール)がありますが、これも字下げから自動的にセミコロンやブレースを挿入する手順が仕様化されており、その結果生成されるトークン列に対して文脈自由文法で解析します。
さらには、XMLやJSONのようなデータ記述言語もBNF/EBNFで厳密に定義され、XMLは文脈自由文法+属性など一部文脈依存の条件(DTDやスキーマで定義)という構成です。
このように、それぞれの言語が目的や特徴に合わせつつも形式的に定義された文法を備えており、その背後ではチョムスキー階層に基づくオートマトン理論や形式意味論の枠組みが活用されています。
形式文法と言語設計の関係
以上、プログラミング言語の設計が数学や形式言語学の理論を基盤として、いかに曖昧性を排除しているかを概観しました。形式文法(とくに文脈自由文法)はプログラミング言語の構文定義に不可欠であり、曖昧性のない厳密な言語仕様を可能にしています。
チョムスキー階層による分類は言語の表現力と解析難易度の指標を与え、実用上はその中の効率的に解析可能なクラス(正則言語や文脈自由言語)が採用されています。
構文解析や意味解析といったコンパイラの各工程にも形式的手法が活かされ、抽象構文木などの概念によってプログラム構造が明確化されています。
またBNFやEBNFによる文法記述、演算子優先順位のルール化、キーワードの予約や構文ルールの工夫によって、言語使用者・処理系の双方に誤解のない設計が実現されています。
プログラミング言語は人間が使うものではありますが、その定義は機械が処理できるよう形式的で厳密でなければなりません。その意味で、プログラミング言語とは計算機科学における人工的な形式言語そのものなのです。
計算機がソースコードを正しく解釈し実行するために、言語設計者は形式言語理論という「言語を扱うための言語」を用いて仕様を定めています。形式文法とオートマトン、意味論の理論によって支えられたプログラミング言語は、曖昧さのない明確なルールの集合として定義され、それゆえにコンパイラやインタプリタによる自動処理が可能となります。
これらの理論と設計原則を深く理解することで、プログラミング言語の持つ厳密さや美しさ、さらには設計上のトレードオフ(表現力と解析容易性など)まで見えてくるでしょう。形式言語学と言語設計は切り離せない関係にあり、今後新たな言語を設計する際にも、これら理論の知見が不可欠であることは間違いありません。
プログラミング言語と数学の深い関係
プログラミング言語の設計は、単なる実用的な工学ではなく、深い数学的基盤に支えられた学問分野です。形式言語理論、オートマトン理論、型理論、圏論など、様々な数学分野の成果が言語設計に活かされています。
この数学的基盤があるからこそ、プログラミング言語は曖昧さのない、厳密な仕様を持つことができます。そして、その厳密さがあるからこそ、私たちは安心してプログラムを書き、それが意図通りに動作することを期待できるのです。
形式言語理論を学ぶことは、単に言語の仕組みを理解するだけでなく、「なぜそのように設計されているのか」という深い洞察を得ることにつながります。それは、より良いプログラマーになるための重要な一歩となるでしょう。
あとがき
プログラミング言語の厳密さの裏にある形式言語理論。その存在を知ることで、日々書いているコードの見え方も変わってくるのではないでしょうか。エラーメッセージ一つとっても、そこには数十年にわたる理論研究の積み重ねがあります。
次にコンパイルエラーに遭遇したとき、それが形式文法による厳密なチェックの結果だと思えば、少し違った気持ちで向き合えるかもしれません。プログラミング言語は、人間の創造性とコンピュータの厳密さを橋渡しする、まさに現代の知的基盤なのです。
本稿を通して得られたアイデアや知識が、あなたのビジネスに少しでも役立つことを願っています。もし、本稿が参考になったと感じていただけましたら、ぜひ「いいね」や「フォロー」をしていただけると励みになります。今後も実践的なノウハウやAI最新動向を共有していきますので、引き続きお読みいただけると嬉しいです。
⚠️ 重要:ChatGPTを使いこなせる人は全体の3割以下
なぜか?「質問力」の差です。
実は、AIから10倍の価値を引き出す「問いの立て方」には科学的な法則があります。Googleが20%ルールで実証し、Amazonのベゾスも推奨する方法。全て『AI時代の最強スキル「好奇心力」』で公開中。
AIが瞬時に「答え」を返す今、勝負の分かれ目は「問い」にある。その秘密と明日の仕事で使える13の実践ツールを凝縮した拙著『AI時代の最強スキル「好奇心力」』もぜひチェックしてみてください。
思考力を劇的に向上させる実践的トレーニング本
ビジネスで成功する人の共通点は「具体と抽象を自在に行き来できる」こと。本書は、この重要スキルを29の実践問題で鍛える画期的なトレーニング本です。
例えば「りんごを抽象化すると?」という問いから始まり、段階的に思考の幅を広げていきます。単なる理論書ではなく、実際に手を動かして考えることで、問題解決力、発想力、コミュニケーション力が飛躍的に向上。
仕事での企画立案、プレゼン、交渉など、あらゆる場面で「なるほど、そういう見方があったか!」という新しい視点が得られるようになります。思考の質を変えたい全てのビジネスパーソンにおすすめの一冊。
プログラマーとしての成長を加速させる必読書
良いコードとは何か?3年目までに知っておきたい、時代や言語に左右されない101の原理原則を凝縮。
「DRY原則」「YAGNI」「KISS」―これらの意味を正確に説明できますか?本書は、プログラミングの世界で長年培われてきた101の重要原則を体系的に解説した決定版です。
単なるコーディングテクニックではなく、設計思想、開発手法、チーム開発など、プロとして必要な「考え方の土台」を網羅。各原則は見開き2ページでコンパクトにまとめられ、忙しい現場でもサクッと確認できます。
ベテランエンジニアが10年かけて学ぶ知識を、効率的に習得可能。コードレビューで指摘される前に、自分で気づける力が身につきます。コードに迷わない基礎力を養う101の原理原則を網羅。
新人〜3年目で差がつく設計思考とベストプラクティスを実例で腹落ちさせます。小手先のテクニックではない、本質的なスキルを身につけたい全プログラマーにおすすめです。
AIとの対話を制する、新時代の必読書
なぜAIは意図を汲み取れないのか?その答えは「言語学」にあります。本書は、プロンプトエンジニアリングの根幹をなす「言葉の仕組み」を解き明かし、誰でもAIを使いこなすスキルが身につく入門書です。
効果的なプロンプトの背後にある言語学的な仕組みを理解することで、AIから望む答えを引き出す力が格段に向上します。
豊富な実例とともに「言葉の構造」「文脈の重要性」「曖昧さの扱い方」などを解説。対話型AI時代に必須の「言語学×生成AI」を平易に解説します。プロンプト設計やモデル理解まで網羅し、非エンジニアでも即戦力になる一冊。
