2016年2月9日火曜日

expressionとstatement

たまにはプログラミング言語についての話をしてみようかと思います。

最近、Twitterで中括弧戦争みたいなのが起きているらしいです。ぶっちゃけ、プログラミング言語なんてコンパイルできれば正義ですし、なおかつ保守性が高いコードを書けるのならば二重丸です。中括弧を付ける場合も付けない場合も、どこかに改行を入れる場合も、それが見やすいと思えるのならばそれで良いでしょう。実際、その辺はコーディング規約とかコーディングスタイルと言った言葉で、各宗派がある程度出来上がっていますし、たいてい、この手の宗派は入門に使った書籍に準じてしまうでしょうから、一緒のプロジェクトをやっているのでもなければ過干渉する必要はないと私は思っています。

しかし、これを機にググってみると中括弧に関する理解の甘い記述が見受けられます。プログラミング言語は自然言語ではないので、「言語ありきで、それについて文法などの説明を後付けする言語」ではなく、「文法ありきでそれをもとに記述する言語」です。
と言うわけで、私のC言語のこのあたりに関する理解について、ここにメモしておこうと思います。

中括弧の省略記法ではない

まず、if文などの制御文についてです。
if(hoge) {
    foo();
}

C言語ではおよそこのような書き方をします。ちなみに、中身が一文のみの場合、中括弧は省略できるとの記述があるものがしばしば見られます。
if(hoge)
    foo();

結果論としては正しいというか、そう見えます。しかし、中括弧が存在するのが原則で、省略記法としてその中身が一文だけなら中括弧が省略できるという理解なのならば、それは不十分です。


この手のプログラミング言語には「式(expression)」と「文(statement)」の概念を持っています。

まず、式とは
  • 値の計算を指定する
  • オブジェクトもしくは関数を指し示す
  • 副作用を引き起こす
の3種類の動作、またはその組み合わせを行うもののことを言います。副作用(side effect)という言葉は関数型言語マン以外には馴染みの薄い表現で、かつマイナスのイメージを持つ言葉かもしれませんが、ここでは「実行環境の状態を変化させる」程度の意味しか持ちません。例えば、変数の値を変更する(ことによって実行環境のメモリの値を一部書き換える)などといったことです。
式は評価した値を持ちます。

一方で、文とは実行すべき動作を規定したものです。JIS規格では
  • ラベル付き文
  • 複合文
  • 式文
  • 選択文
  • 繰返し文
  • 分岐文
の6種類が文として定義されています。詳しくは割愛しますが、ラベル付き文、式文、分岐文はセミコロンで終わる文法になっていて、選択文や繰返し文はいわゆるif文やwhile文など(分岐や繰り返しの終了地点まで含んだもの)で、複合文は中括弧で囲まれたブロックとなっています。式文はその名の通り式が入る文で、ここに空文も含まれています。分岐文はbreakとかcontinueとかreturnとかですね。


これらを踏まえた上で、if文の構文がどう定義されているかと言うと、
if ( 式 ) 文
です。 いたってシンプルです。

ここから導き出されるのは、中括弧無しは一文の場合の省略記法ではなく、式文で書くか複合文で書くかの違いに過ぎないということです。並列していくつもある「文」のうち1つを書けるという話なので、「原則が中括弧」みたいな発想はその人の固定観念ということになってしまいますね。


カンマ演算子

複合文は0個以上のをまとめて1つのとして扱うものですが、C言語には複数のを1つのとしてまとめる「カンマ演算子」と言うものがあったりします。ほぼほぼ出番が無い演算子ですので、知らない人も多いかもしれません。

カンマ演算子は次のような構文を持ちます。
式, 式, 式, ... , 式
2個以上の式をカンマでつなげるだけです。なお、この式の評価した値は最後の式(一番右側の式)となります。
int i, j, k;

k = (i = 0, j = 1); 
printf("i = %d, j = %d, k = %d", i, j, k);

このコードはコンパイルでき、実行すると
i = 0, j = 1, k = 1
という出力が得られます。 これを上手く使うことで、複合文のような記述を式文を使って作ることができます。
if(hoge)
    puts("hoge"),
    puts("foo");
else
    puts("bar");

ifの1つ下の行のputsは、末尾がセミコロンではなくカンマになっています。すなわち、2行目と3行目の2つのputsを合わせて1つの式にしているわけです。そのため、ifの直下の文は1つの式文になるため、これはコンパイルが通ります。もしもifに与えた条件式hogeが真ならば、hogeとfooが両方とも画面に出力されます。

この記法は中括弧で複数文をくくるより果てしなく見にくいです。と言うより、カンマかセミコロンかなんてパッと見で区別できる人なんてなかなかいません。そのような観点から、使われることはまずないと思われますが、たまに関数マクロなんかで上手く使えば問題の起きにくいマクロが作れたりしてそのような場に出番があると言えるでしょう。

else if文は存在しない

さて、C言語の選択文の項目には、実際には3種類の構文が規定されています。
  • if ( 式 ) 文
  • if ( 式 ) 文 else 文
  • switch ( 式 ) 文
見出しでネタバレしちゃっていますが、そうです、C言語に「else if文」と言うのは存在しません。しかしよく「else if」と記述する人はいますし、実際それでもコンパイルは通ります。
実はこれ、if文全体が1つの文と見なされることを利用し、else以下の文としてif文を置いただけということになります。すなわち、else ifはこのように書くのと同値です。
if(hoge) {
    foo();
} else {
    if(hogehoge) {
        foobar();
    } else {
        fooooo();
    }
}

一般的にこのような記法は無駄にインデントが増えるだけで好まれません。「if文の条件が成り立たなかったときに次の条件が成り立つかを判定する」という概念を1つ頭の中に入れるだけでelse ifという記述は読みやすくなりますから、その記法に反対する人はいないでしょう。しかし、言語仕様ではあくまでも「else以下の文をif文とした記法」となっているわけです。

それを踏まえると、else whileやelse switchなどの記法も同様に文法上書けることになってしまいますし、
if(hoge)
    while(hogehoge) {
        foobar();
        fooooo();
    }

といった記法も許されます。
読みやすいかどうかは別として、どこまでが文で、どこまでが式かと言うことをしっかりと理解しておけば、プログラムを見たときの脳内コンパイルは捗るでしょう。

C#のusing文

C#におけるusing文は、上記のif文同様に
using ( リソース取得式 ) 文
という構文になっています。もちろんusing文自体も文の1つにカウントされるため、using以下にusingを書くことができます。
using(StreamWriter sw = new StreamWriter("dst.txt"))
using(StreamReader sr = new StreamReader("src.txt"))
{
    while((line = sr.ReadLine()) != null)
        sw.WriteLine(line);
}

このように書くことによって、複数のIDisposableインスタンスを作って最後にすべて破棄するという動作を見た目上やっているようになります。本来ならば2つ目のStreamReaderのところはインデントを1段下げるべきなのでしょうが、特に並列的に書いて問題ないところですし、逆に下げると見にくくなるので敢えて下げていません。
下手したら、「usingは並列にいくつも書ける」みたいな説明をしだすところもあるかもしれませんが、特段文法上そういった定めをしているわけではなく、これはC言語系統の文法の体裁として、むしろ一般的な記述であるわけです。

まとめと私見

ここまで読んで、C言語系の言語のソースコードを見る目が変わった方もいるかもしれません。今まではなんとなく書いていたけど、今となればどれが文でどれが式かを意識しながら書けるようになっているかもしれません。

そもそも、プログラミング言語は、0と1のみで表されるコンピューターへの命令を人間にわかりやすい方法で書けるようにしようとして発展してきたものです。今は高度に抽象化され、さらに様々な記法やパラダイムで多種多様な形態を持っていますが、最終的に動作するコンピューターの仕組みは原則として同じです。そして、モダンなプログラミング言語はそのコンピューターの仕組みを知らなくてもできるだけ簡単に書けるように敷居を下げた一方で、やはりハイレベルなプログラミングにはどうしてもそういった知識は付いて回ります。
おそらくプログラミング言語に関してもそれは同じで、一般的な書き方、一般に可読性が良く保守性が高いと言われる記法はありますが、それより一歩進んでコンパイラーがどのように構文を解析し理解するかを頭に置いてプログラムを書くのはとても大事なことだと言えるでしょう。

私は、たまたまC言語を始めるにあたって巡り合った入門書に比較的そういった記述が多く、このような考え方を非常に強く植えつけられながらC言語を理解していきました。そのため、if以下の文などに中括弧を付けるかどうかの議論で「省略したほうがスマート」とか「省略したら可読性が下がる」みたいな意見を見るたびに「そもそも省略じゃないよねそれ」という気持ちになってしまいます。


私は、ifの下を必ず複合文にするかと言うと、そうではありません。むしろ、1つしか式を書かない場合は積極的に式文を使います。
「if文の中身を複数文にしたくなった時に中括弧を付け忘れたらバグになるじゃないか」という意見をたまに見ますが、私はそう思いません。インデントはIDEがちゃんと整理してくれますから、インデントだけ付けて中括弧を付け忘れる状態に気づかないということもまず無いですし、そもそも変更箇所の前後の中括弧が目に留まらないということもまずありません。むしろ、中括弧で1行を使ってしまうとプログラムの縦方向の密度が無駄に下がり、可読性が下がると考えています。
ただし、ifの直下にwhile文だけが入る場合など、一文でも複数行にまたがる場合は基本的に中括弧を入れています。すなわち、ほぼほぼ行数で決めているといったところでしょうか。

一方で「if(式)」とその「中身の文」の間に改行を入れない記法はまずしません。
if(input < 0)  return ERROR;

このような記法は、まあこれくらいなら良いのかもしれませんが、条件式が少しでも長くなると可読性が一気に下がるので私は好みません。たとえ空文でも次の行を1行使います。


結局、この辺りは好みだと思います。自分がC言語を習得したときの入門書、もしくはこれまでに使っていたプログラミング言語の風習、自分の感性、そういったものが合わさって「自分はこう書く」みたいな話が出てくるものでしょう。

大事なのは、言語の仕様を理解し、それに即し、また可読性と保守性が良くなるように自分で工夫を重ねることだと思います。そういったところから、技術力の高さをアピールしていきたいですね。