- C言語には「式」と「文」の区別があり、
ifやforは値を返さない文として設計されている。 - すべてが式であるLispなどとの違いは、C言語が実用目的で作られた経緯や、ALGOLやBCPLの影響による。
- RustやKotlinなどの後継言語では
ifが値を返す式として設計されており、式指向への移行が進んでいる。
1. ifが値を返せれば……
C言語には「式」と「文」の二種類があります。
たとえば、a + b は式で値を返すが、if や while は文であり値を返しません。
同じ時代に存在したLispは、コードの構造が式しかない1。
(if (> x 0) x (- x))Code language: Lisp (lisp)
これは、条件によって二つの値のどちらかを選ぶ、という操作をそのまま書いた形です。if も let も + も、すべてこの S式 の形をとり、値を返します。。
一方、Cの if は文です。
値を返さないので、同じことをするには一度変数に入れるのが基本です。
int result;
if (x > 0) {
result = x;
} else {
result = -x;
}Code language: Arduino (arduino)
if が式になっていないのは、標準化プログラミングの概念を生み出した ALGOL に由来します2。
ちなみに、Lispと同じことをするなら、ifではなく、三項演算子 x > 0 ? x : -x を使うことができます。
これはCの中で例外的に「式として書ける条件分岐」として用意されたものの一つです。
1.1. ループと再帰
Cの for も文であり、ループ全体として値を返しません。
合計を求めるには変数を外に置く必要がある。
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
printf("%d\n", sum);Code language: Arduino (arduino)
これは、Lispでは loop の結果はそのまま式の値として得られます。
(format t "合計: ~a~%" (loop for i from 1 to 10
sum i))Code language: Lisp (lisp)
Cで同じ構造にしようとすると、ループを関数に切り出すか、変数を経由するしかありません3。
1.2. returnの必要性
Cでは、関数から値を返すために return を明示します。
int square(int x) {
return x * x;
}Code language: Arduino (arduino)
この return も「この値を返して関数を終了する」という文。
書き忘れると未定義動作になります4。
Lispでは関数の本体が式であり、その値が自動的に返り値になる。
(defun square (x)
(* x x))Code language: Lisp (lisp)
* の結果がそのまま square の返り値になります。
複数の処理が並ぶときも、最後の式の値が返されます。
「返す」という操作を別途書く必要がありません。
2. 計算機と副作用
C言語が式と文を分けているのは、理論的なものではありません。
設計者のRitchie自身、後年のインタビューで「その場その場の判断の積み重ね」と語っています。
彼が C を作ったのはPDP-11の上でUnixを動かすため。
そこでは、理論的な美しさより「これで今すぐ動かせるか」が優先されました。
たしかに、数学の関数は同じ入力に対して常に同じ値を返します。f(x) = x + 1 は何度呼んでも世界は変化せず、値を「求める」のがその機能(function)です。
一方、計算機は違います。
メモリに書き込み、ファイルを変更し、画面に出力する。
これらは、世界を変える操作で、数学には対応物がありません。
計算機では、この副作用が主役なのです。
C言語の printf も scanf は、返り値は持ちますが普通は無視されることと同じです。
これは、より低水準のアセンブリでも同様です。
処理の結果は特定のレジスタに置かれますが、呼び出し側がそれを読むかどうかは自由です。
読まないで捨てられる値もたくさんあります。
Cの「関数」はその名前にもかかわらず、主な目的は値を生むことではなく、処理を起こすことです。
これは、本質的には「手続き(procedure)」です。
2.1. ALGOL由来の制御文構造
「制御構造を文として書く」発想は、ALGOLやBCPLにも由来しています。
ALGOLは制御フローを「文」として定義した言語です。
このとき、if や for は値を返さない構造として設計されました。
Cの直接の先祖にあたる、BCPLは、そのALGOLの影響を受けつつ低水準操作を書けるようにした言語です5。
3. 式は便利
「命令型言語でも式を返せた方が便利」という認識は広まっており、RustやKotlin、Scalaでは if が値を返す6。
C系の後継言語では、少しずつ式を第一にする設計に近づいています。
C言語が文を持つのは歴史的・設計的な選択の結果なのです。
- LispはJohn McCarthyが1958年にMITで設計した。コードとデータが同じ形式(S式、Symbolic Expression)で表現されるため、プログラムがプログラム自身を操作できる。 – Lisp (programming language) – Wikipedia
- ALGOL 60は1959〜60年に国際委員会が設計した言語で、
ifやforを値を返さない「文」として定義した。構造化プログラミングの概念を初めて体系化した言語の一つとされている。 – ALGOL 60 – Wikipedia - ここで使っている
loopマクロはCommon Lispの機能で、ANSI Common Lisp(1994年標準化)で定義されている。Schemeなど他のLisp方言では末尾再帰最適化を使った再帰関数が一般的なループの書き方になる。 - C17標準(ISO/IEC 9899:2018)§6.9.1.12に「関数を終了する
}に到達し、呼び出し元がその戻り値を使用した場合、動作は未定義」と定められている。コンパイラはエラーではなく警告を出すにとどまる場合が多い(-Wallで-Wreturn-typeが有効になる)。 – MSC37-C – SEI CERT C Coding Standard - BCPLはMartin Richardsが1967年にMITで設計した。CPLから「コンパイルを難しくする機能を取り除いた」言語として生まれ、型システムを持たず全データをワード単位で扱う。KenThompsonがこれをもとにB言語を作り、RitchieがそこからCを開発した。系譜はCPL→BCPL→B→Cとなる。 – BCPL – Wikipedia
- 「式指向プログラミング言語」はすべての構造が式として値を返す言語の総称。Lisp、ALGOL 68、ML、Haskell、Ruby、Rust、Scala、Kotlin、OCamlなどが該当する。Cは該当しない。 – Expression-oriented programming language – Wikipedia