文法

提供: langdev
移動先: 案内検索

プログラミング言語のパラダイム[編集]

プログラミング言語にはいくつかのパラダイムが存在する。それらは概ね排他的なものではなく統合や混合が可能である。

以下にいくつかのパラダイムを挙げる。

手続き型 
概ね逐次実行されることを前提に計算の手順を記述するもの。
関数型
理念的にはプログラム全体を通しての状態(大域変数)を持たず、また関数からさらに関数を呼び出す形式で記述するもの。
宣言型
概ね関数型に分類される言語と、Prologを含む。手続き型も上記の関数型(特にLisp)は形式はともかく計算手順を記述しているが、それに対して計算の定義を宣言するもの。
論理型
概ねProlog。ただしPrologを論理型と呼ぶのには異論もある。
Concatenative型 
概ね演算子などを対象に後置するものであり、かつスタックを用いる。Forthがその代表。ただし、後置やConcatenativeであることを純粋に追求すると、必ずしも読みやすいプログラムとはならなくなる。例えばFactorを参照されたい。
オブジェクト指向 
データとそれに対する処理をカプセル化するという思想を持つもの。その思想の反映の強さには様々なものがある。

文法の構成要素[編集]

プログラミング言語が採用しているパラダイムによって、いくらかの違いはあると思われるが、言語の構文を構成する構造には概ね以下のようなものがある。

  • 計算順序の指定(一般には逐次)
  • 代入
  • 条件分岐
  • ジャンプ
    • ループ
    • 関数/手続き呼び出し
    • 再帰
  • 関数/手続き、ないしサブルーチンの定義

構文糖[編集]

プログラミング言語の文法を俯瞰する際に注意することとして、構文糖がある。

構文糖とは、あるプログラミング言語の正規の文法、あるいは理念的な正規の文法に対して、プログラムを書きやすく、あるいは読みやすくするために導入されている簡潔な書き方を言う。

構文糖により、ある言語においてプログラムを書きやすく、あるいは読みやすくなる。しかし構文糖はその言語の理念からは外れたものである場合がある。自然言語や数式、そしてそのプログラミング言語より前から存在するプログラミング言語から受け継いだ慣習を反映している場合がある。

理念上の正規の文法と構文糖を区別しなければ、文法に矛盾を感じたり、あるいは文法がきちんと設計されていないように感じることもあるだろう。

計算順序[編集]

多くの言語においては、ソースコードに書かれている順番に実行をするか、あるいは"main"のような特定のキーワードが名前となっている関数から実行を開始する。

RubyPerlの場合、原則として書かれている順番に解釈される。さらに、classや関数の定義の間に、通常の実行文を書くことも可能である。このような場合、Perlのようにソースを全て解析した後に実行するインタプリタであれば問題とはならないが、しかしRubyの場合、あくまで書かれた順番に実行していくため、ある関数を呼び出すには、それが呼び出されるより前に定義が実行されていなければならない。

また、Rubyにおいては、あるスクリプトを単体で実行する場合と(そのスクリプトが別のスクリプトをrequireすることはある)、ライブラリのようにrequireによって別のスクリプトに読み込んで用いる場合とがある。特にそのスクリプトのテスト段階では、そのスクリプト単体での実行が必要となる。その場合には以下のように書くことにより、スクリプトそのものが実行されている場合にのみ実行される部分を記述することが可能である。

if __FILE__ == $0 
    略 
end

この点はPythonでも似た仕組みが用意されており、次のように書くことにより、そのスクリプト自身が単独で実行された場合にのみ実行する部分を書くことが可能である。

if __name__ == "__main__": 
    ブロック本体 

REBOLにおいてもあくまで書かれた順番に実行される。

RubyPythonもインタプリタ型言語であるが、それらとは異なり、インタプリタが謂わば実行環境を提供している場合がある(Rubyでいえばirbが標準で実行系に組み込まれているようなものである)。Lisp系の言語はその代表であろう。環境内において実行を指定するため、既に定義されている関数であれば、原理的にはどの関数からの実行も可能である。この特徴は、Lisp系言語に限らず、MLOCamlHaskellPrologなども同様である。

とは言え、その実行環境内においてのみプログラムの実行が可能であるという特徴は便利であるとともに、プログラムをフィルタとして用い、なんらかのデータを複数のフィルタを通して加工ないしなんらかの処理をするという、基本的なプログラムの利用方法から考えれば不便な制限でもある。これは書かれた順番に実行されるのでもなく、原理的には特権的な名前の関数などが存在しないという場合に問題となる。そこで、Lisp系であるSchemeの実装であるGaucheにおいては、コマンドラインから実行する場合には"main"関数から実行が開始される。これはHaskellの実装であるghcにおいても、"main"関数を定義することで同様の動作が行なわれる。特にghcの場合、コンパイルも可能であるが、コンパイル結果は"main"から実行される。いずれも、"main"という名前を特権的なものとして扱うようにしたものである。

ただし実行順序という話題には必ずしも収まらないが、MLOCamlHaskellPrologなど「宣言型」と呼ばれる言語の場合、同じ名前の関数を引数の数やその型の違いによって複数定義することが可能であったり、関数の定義自体は1つであってもその中では先の条件により定義がわかれている場合がある。これは他の言語にはあまり例を見ない特徴であろう

どこから実行されるか以外の問題、例えば定義された関数などの内部での実行順序について言えば、おおよその言語は逐次実行が原則である。対してLisp系は、ある関数をまず実行するのだが、その関数を実行するためにはその関数の中で使われている関数を実行する必要があり…と、特殊形式の場合を除いて、謂わばカッコの内側から実行されているように読める。なお、Lisp系においては"prog"ないし"progn"のような特殊な関数が用意されており、その内部においては他の逐次実行のプログラミング言語と似た記述が可能である。

おそらくこの話題については、チューリングマシン(あるいはより現実的なモデル)と、ラムダ計算、またPrologについてはWAM(Warren's Abstract Machine)などの計算モデルに言及することでより理解が深まるだろう。

代入[編集]

代入とは、静的・動的に用意される変数領域に値を入れる操作である。言語によっては、必ずしもそのような実装ではなく、変数と値との束縛(bind)によって概ね同様の効果を得ている場合もある。

=[編集]

CJavaRubyなど多数 一番多そう。

比較に見えるという人もいる。

 a = 3 


:=[編集]

Pascalなど

通称ちんこ演算子

 a := 3 

let[編集]

Lisp系、古いBASICなど (ひとくくりにして良いかはわからない)

冗長に見えるが、パースがしやすい。

ただし、言語によってletの意味や動作は異なる。

BASIC[編集]

次の場合、単純な代入である。

 LET $A = 3 

Lisp[編集]

Lispにおいて、おそらくもっとも基本的な代入処理は"set"ないし"setq"によるものである。

 (setq a 3.14) 

Lispの処理系によっては、変数の代入には特に"setvar"を用意しているものもある。


 (setvar a 3.14) 

Lispにおける、"let"はそのスコープの範囲内において有効な局所的変数を宣言する。この例では局所変数aが宣言され、同時にその初期値として3を割り当てている。

 (let ((a 3)) (display a)) 

(この式は意味をなさない。実行しようといてもエラーになる。) しかし次のように書いた場合は、"let"のスコープ内に局所変数a, b, cを用意するという意味になる。この局所変数を使うには、別途代入が必要である。

 (let (a b c) (+ a b c)) 


NewtonScript[編集]

NewtonScriptの次の例を見てみよう。 代入ではないがそれに準じるものとして":"についても触れる

aFrame:= {
  foo: 10,

  bar: func(x) begin
    if foo then Print (“hello”);
    if x > 0 then begin
      local foo; //local variable to function
      foo:= 42;
    end;
    return foo;
  end;
}
NewtonSctiptにおいてはPascalと同様に
 := 
を用いる。ここでは"aFrame"にそれに続く"{ }"の中身を割り当てる。

なお、フレーム内に"foo:"のように":"が現れている場合の判断は難しい。フレーム内における":"の使用ルール、あるいはフレームを構築するルールは次のようになっている。謂わば、語末に":"が突いているものは連想配列のキーとでも思えば良いだろう。

{ <symbol>: <expression>, <symbol>: <expression>, …}

このような場合、symbolに":"を付け加えて記述した場合、"シンボル"-"値"の組を記述しているのであって、代入ではない。だが、フレーム内における、例えば"foo"の扱われ方や、フレーム外部から関数"bar"を呼び出すが可能であることなど、実質的には代入と同等と考えてよかろ。

REBOL[編集]

REBOLの例を見てみよう。REBOLでは概ね":"を代入記号に用いる。次に例を挙げる。

example: make object! [
  var1: 10
  var2: var1 + 10
  var3: now/time

  set-time: does [var3: now/time]

  calculate: func [value] [
    var1: value
    var2: value + 10
  ]
]

これは、"object!"以下のブラケットの中身をオブジェクトとして、"example"に代入(?)している。

ブラケットの中は、"var1: 10"は「var1に10を代入する」ことを意味する。

また、"calculate"の部分は、"func"のあとの部分の最初のブラケットでは引数を宣言し、次のブラケットでは関数本体が記述されているとし、それを関数として"calculate"に代入(?)している。

"var1:"など、NewtonScriptのフレーム内での記述形式に似ていが、REBOLにおいては":"はどこに有っても代入演算子である。

swift[編集]

swiftにおいては、letを用いた上記のような代入式は、定数の割り当てを意味している。定数を割り当てられた変数は、後にその内容を書き換えることはできない。

let π = 3.14

その他[編集]

Smalltalkの処理系によっては _ や ← を使うらしい。また、R言語では <- や <<- をつかう。

 a ← 3 

また、通常の代入と変数宣言を伴う代入を別の方法で行う言語もある。 例えば、Go言語では、変数宣言をvar文で行い、 = を代入に使うが、 := を変数宣言と代入両方に使う。

var a = 3
b := 4
a = 5
b = 6

改行・インデント[編集]

一部の言語では改行・インデントに意味がある。

FORTRAN[編集]

現在、ソースコードにおける改行や空白文字は意味を持たない言語が多い。それらにおけるインデントと改行は主に人間にとっての読みやすさのために利用される。だが、その場合、コンパイラやインタプリタは原理的には無限長の文字列を読み込めなければならない。例えば、巨大な関数、クラス、ループなどにおけるブロックを想定すれば明らかであろう。

しかし、Fortranコンパイラは当初そのようには作られていなかった。コーディングは基本的にカードにコードを記述することを前提に設計されていた。その場合、一行に収まる文字数は(欄を無視しないのであれば)物理的に制限されている。そのカードに基づいてコードを入力するが、その際も一行の文字数は制限されていた。そのため、一文ないし1命令の記述が複数行にわたる場合、2行目以降の各行の1文字目には"*"を入力するなど、「続きであること」を明示する必要があった。これは逆に言えば、各行の1文字目には、そのような標識以外は書いてはならないという制約でもあった。

ABC[編集]

オランダの国立情報工学・数学研究所 (CWI) において開発された、主に教育用を目的とした言語である。インタプリタおよび環境として提供されていた。

インデントを用いてブロックを表す記法を採用していた。

Pythonの開発者である Guido van Rossum も開発に参加した経緯があり、PythonがABCの影響を受けていると述べている。

Python[編集]

Pythonは、最もインデントに依存した文法を持つ言語の一つである。クラスや関数の宣言は開始行からその後のインデントされたブロックを範囲として扱う。 改行はNEWLINE、インデントの増加はINDENT、インデントの減少はDEDENTというトークンになり、文法はそれらのトークンを使って定義される。ただしバックスラッシュの後の改行はNEWLINEとならないし、各種括弧の中の改行もNEWLINEとならない。空行も無視されNEWLINE, INDENT, DEDENTを生成しない。ただしインタラクティブなREPLでは完全な空行は複数行の文の終了を表す(実装依存)。

ブロック(suite)は改行とインデントで示す方法と、1行の中で文をセミコロンで区切って示す方法がある。ただし1行の中で文をセミコロンで区切って示す方法ではif文などをネストできない。

Haskell[編集]

文法上では波括弧とセミコロンで構造を示すよう定義してあり、省略されている場合はインデントや改行から波括弧とセミコロンを補う。where, let, do, ofの後が波括弧以外であれば開き波括弧を補い、インデントと改行によりセミコロンと閉じ波括弧を挿入する。

改行やインデントの利用により、ブロックの開始や終了にあまりキーワードや記号を使わなくてもよいように文法が工夫されている。

ECMAScript[編集]

文法上ではセミコロンで文の区切りを示すよう定義してあり、省略されている場合に改行からセミコロンを補う。セミコロンの挿入規則は次の通りである。

プログラムを左から右に構文解析する際に、文法の導出規則から許されないトークンが出現した場合、次の規則を満たせばそのトークンの前にセミコロンを補う。

  • 出現したトークンが前のトークンと1つ以上の改行で区切られている
  • 出現したトークンが}である。

プログラムを左から右に構文解析する際に、入力ストリームを表すトークンが現れ、ストリーム全体をECMAScriptの完全なプログラムとしてパースできない場合、入力ストリームの最後にセミコロンを補う。

プログラムを左から右に構文解析する際に、文法の導出規則から許されるトークンであるが、導出規則がrestricted productionであり、トークンが"[no LineTerminator here]"という注記の直後に来る終端記号や非終端記号の最初のトークンになれる場合、そのトークンが前のトークンと1つ以上の改行で区切られている場合、セミコロンはそのトークンの前に補われる。

ただし、挿入したセミコロンが空文になる場合や、forのセミコロンとなる場合は挿入されない。

セミコロンを省略した例: [EOF]は入力ストリームの最後を表す。

 var x = 1
 return x
 console.log(1)[EOF]
 {console.log(1)}
 return
 console.log(1)

セミコロンを省略しない等価な例:

 var x = 1;
 return x;
 console.log(1);[EOF]
 {console.log(1);}
 return;
 console.log(1);

Ruby[編集]

Scala[編集]

改行は条件によって特別なトークンnlとなる。各文はセミコロンかnlで区切られる。 2つ以上の改行がnlになる場合、通常1つのnlにまとめられるが、条件によっては複数のnlとなり、1つの改行と2つ以上の改行でプログラムの意味が変わる場合がある。例えば、次のコードは匿名クラスを表す。

 new Foo
 {
   ...
 }

一方、次のコードは2つの式(インスタンスの作成とブロック)である。

 new Foo
 
 {
   ...
 }

条件分岐[編集]

条件分岐の記述は、その基礎においては次のようになる。

if 条件 then begin
   条件が真の場合の実行部
end

ここでは「条件が真の場合の実行部」と書いているが、条件が偽の場合であっても「条件の否定」を条件として書けば、条件が偽の場合に実行する部分を記述できる。

さらに、条件が真の場合には特定のアドレスにジャンプするとすればコンパイル結果のバイナリやインタプリタの内部VM用コードがif then begin endに対応する構造を持つ必要はない。

とはいえ、条件が真の場合の処理のみを書けるのでは不便である。そこでこれは次のように拡張される。

if 条件 then begin
    条件が真の場合の実行部
else
    条件が偽の場合の実行部
end

初期のBASICにおいては、「条件が真の場合の実行部」などに複数のステートメントを書くことができなかった。そこで、その代わりにgoto文を使い、真の場合、偽の場合のジャンプ先を示していた。

条件が常に真と偽のみの値だけで2分岐できればいいというわけでもない。そこで、さらに次のように拡張される。

if 条件1 then begin
  :
elseif 条件2 then begin
  :
elseif 条件3 then begin
  :
  :
else begin
  :
end

ここで"elseif"の書き方は言語によって差異がある。"else if"と書く言語もあれば"elsif"と書く言語もある。

だがこの場合、条件に漏れがないかどうかの確認が重要である。

また、この書き方は面倒でもある。そこでこのような3つ以上の多重分岐の場合には別の書き方が推奨あるいは用意されている場合もある。"case", "switch", "cond"などである。C言語のswitchの場合、つぎのようになる。

switch (式) {
  case 定数1:
    :
    break;
  case 定数2:
    :
    break;
    :
  default:
    :
    break;
}

Pascalの場合は次のようになる。

case 変数 of
    定数1 : begin 
            :
           end;
    定数2 : begin
            : 
         end;
      :
    else begin
        :
    end
end;

さて、case内部の条件に定数しか書けないのは、安全であるとともに不便でもある。そこでLispの場合をみてみる。

(cond
  ((条件1) (実行部分1))
  ((条件2) (実行部分2))
  (t       (実行部分3))
)

この場合、各々の条件は互いに全く関係のないものを書くことも可能です。また最後の"t"はLisp処理系によって書き方が違う場合があります。この場合、"t"はtrueを意味しており、その評価において常に真になることをりようし、elseに相当する部分を記述します。

ジャンプ[編集]

ここでは、単純な"goto"に加え、ループ、イテレータ、map、関数の呼び出し、関数の再帰呼び出しもジャンプに含めて説明する。

goto[編集]

関数呼び出しやメッセージ・パッシングによらず、「次の実行箇所」を指定ないし変更する際の、もっとも基本的な命令が"goto"である。

昔のBASICにおいては、サブルーチンを作ることもできなかった。しかし、あるまとまりのある処理を一箇所にまとめて書くという技術あるいは考え方は存在していた。そのような書き方をしても、サブルーチンの名前などを用いてその部分を呼び出すことはできなかった。そのため、プログラムを構成する行の番号(あるいはラベル)への"goto"(ジャンプ)という極めて直接的な書き方がなされていた。だが、このような書き方はプログラムを読みにくいという根本的な理由により、構造化プログラミングが提唱されるようになっていった。

構造化プログラミングの時代になっても、実際には"goto"が完全に排除されたわけではない。例えばC言語においても"goto"は存在している。しかし、特殊な場合にのみ使うことが推奨されている。

必ずしも"goto"と同等と言えるわけではないが、イベントをキャッチしての割り込みの定義や、Javaなどにおける"try ... catch..."などは構造化プログラミングというパラダイム内において、特殊なジャンプをいかに許容、あるいは形式化するかという方法とも言えるだろう。

ループ、イテレータ[編集]

ループには概ねforループ、whileループ、do...whileループがある。whileループとdo...whileループとの違いは、do...whileループはループ部分が最低でも1回実行される点である。

for、while、do ... whileの3つのループがあるが、ループにおいては3つの形式が必要なわけではない。forにおけるループカウンタあるいは条件にどのような処理を記述できるのかにもよるが、原理的にはループの形式は1つあれば十分である。例えば、whileループがあれば十分である。複数の形式が定義されているのは、およそ人間にとってのわかりやすさのためのみである。

whileループでforループを実現する際の例を挙げよう:

i = 0;    /* カウンタ */
while (i < 10) {
    pintf("%d\n", i);
    i++;
}

whileループでdo...whileループを実現する際の例を挙げよう:

i = 0;    /* カウンタ */
printf("%d", i);    /* do...whileの中身を先に1回実行している */
i++;
while (i < 10) {
    printf("%d", i);
    i++;
}

for ループ[編集]

forループ、whileループ、do ... while ループは厳密な用語ではない。

forループは、ループカウンタを用い、ループカウンタの開始値から終了値まで、指定された範囲に記述された処理を繰り返す。ループカウンタは、インクリメント、デクリメントが概ね可能であり、その際のステップ(差分)も概ね指定可能である。

ループカウンタを機械的にインクリメント、デクリメントするだけでなく、ループ内部においてループカウンタの値を変更することにより、特異な動作を目指す場合もある。これは便利であると共に危険でもある。

あるいはループカウンタではなく、条件を用いることが可能な言語もある。

while ループ[編集]

whileループは、ループの開始時に条件を確認し、条件が真の場合にはループ内部を実行するものである。


do ... while ループ[編集]

do ... while ループは、ループの部分を実行した後に、条件を確認するものである。

ややこしいが、その条件が真であればdoからのループ部分を繰り返すというものと、その条件が偽の間はループを繰り返すという言語がある。後者の場合にあたるのがPascalである。その例を次に挙げる:

repeat
    statements;
until 条件;

whileループとの違いは、ループ部分が最低でも1回実行される点である。

イテレータ[編集]

(イテレータの範囲は有限であり評価遅延はないものとする。)

ループ本体に渡す値などの列挙をまず行なう(遅延評価が必要な場合もある)。この後には2種類の方法がある。もしループの各処理があり、もう一つが先のイテレーた処理よって複数の値が得られた場合絵ある。偉えた値をループ本体に渡すものである。それに対しその部分の情報がになる必要にまでその部分の評価お行わないおいう方もあり、遅延評価と呼ばれている。

map[編集]

mapは、与えられた要素(ないし値)に対して、与えられた処理を適用するという点においてイテレータと似ている。しかし、「与えられた処理」として関数を与える高階関数である点が異なっている。なお、「与えられた処理」にはlambdaなど無名関数を与えることもできる。

(map 'list (lambda (x) (+ x 10)) '(1 2 3 4))

この結果は次のようになる。

(11 12 13 14)

関数呼び出し[編集]

あまり意識されないかもしれないが、関数呼び出しもジャンプの一種である。

関数呼び出しにおいて最低限必要な記述は、呼び出す関数の指定である。その指定は、関数名、あるいは無名関数の記述によって行なわれる。

しかし、関数名の指定のみを行なう場合、計算は大域変数か呼び出し元の局所変数を操作することになる。あるいは、関数の処理のみに依存する結果を求める場合となる。関数は概ねなにがしかの値を与え、それに関連した処理を行なって欲しいので、関数名の指定に加えて、引数が与えられることになる。

更に、返ってくる値の型を示すことにより、コンパイル時、あるいは実行時のエラー検出が可能となる。この型指定は実際には概ね関数の定義の際に行なわれる。

そこで、次のような形式が一般的なものとなる:

返り値の型 関数名 引数の並び

関数呼び出しにおいては、現在実行している関数での局所変数や、呼び出した関数から戻ってくる際の復帰場所を維持、記録するとともに、呼び出す関数が用いる(デフォルトの)局所変数用メモリ領域などを確保した上で、呼び出し先の関数へとジャンプする(関数の引数などの処理も行なわれる)。

昔のBASICなどにおけるサブルーチンはそのような処理を行なっていなかった。そのため、変数の扱いには注意が必要であった。

また、Pascalにおいては、返り値がある関数と、返り値がない手続きが区別されていた。

なお、「局所変数用メモリ領域」と書いたが、Algolにおいてはそのような手法は用いていなかった。その代わりに、コンパイル時にソースコードの局所変数の名前を、衝突が起きないように書き換えていた。この手法は、次に述べる再帰呼出しにおいては重大な問題となることは明らかである。

関数の再帰呼出し[編集]

関数の再帰呼出しとは、関数が自分自身を呼び出すことである。ただし、関数Aが関数Aを呼び出す場合だけでなく、関数Aが関数Bを呼び出し、関数Bから関数Aを呼び出す場合などもある。言語によっては、ループよりも再帰を用いることが好まれる。

再帰呼出しは関数呼び出しの一種であるが、上で述べたように、関数呼び出しは"goto"などの単純なジャンプに比べてコストが高い。しかし特定の場合にはそのコストを抑えることができる。そのような事が可能な再帰の代表例は「末尾再帰」である。リンク先のLispのコードが分かりやすいと思うが、処理の最後で再帰呼出しを行なう場合である。このようなコードでは、再帰からの復帰は、計算結果の値を持って呼び出しを遡るのみである。その性質を利用し、ループへの書き換えや、復帰のための情報の保持の簡略化ないし削除により、計算資源の節約や高速化が可能となっている。

メソッド呼び出し/メッセージ・パッシング[編集]

オブジェクト指向における用語ないし概念である。

関数呼び出しの一般的な形式は「返り値の型 関数名 引数の並び」であった。それに対し、オブジェクト指向の場合は、オブジェクトのメソッドを呼び出す、あるいはオブジェクトにメッセージを送るという考え方になる。

形式としては次のようになる:

オブジェクト.メソッド 引数の並び

ここで「オブジェクト」は、あくまで見た目での話ではあるが、先の関数呼び出しの際の引数の一つが外に出ているようなものだと思えばよい。ただし、理念上は、あくまでオブジェクトに対しての操作ないしメッセージである。

言語は特定しないが、関数呼び出しが次のようになるとしよう。ここで、a, bはともに整数型であるとする:

int add(a, b)

それに対してメソッド呼び出し、あるいはメッセージ・パッシングは次のようになる:

a.add(b)

語感ないし言語感覚の問題があるが、この場合関数呼び出しを日本語で表現するならば「aとbを足す」となり、メソッド呼び出しは「aにbを足す」と表現できよう。関数呼び出しの場合、aとbは同等の扱いであるのに対し、メソッド呼び出しではaというオブジェクトが主体であり、そのオブジェクトのメソッドを呼び出す、あるいはそのオブジェクトにメッセージを送ると考える。