プログラミング作法
2023-02
プログラミング作法(programming style)の記事では、コンピュータ・プログラミングおよびプログラムのスタイル(書法)についての話題を述べる。この分野の古典は、1970年代の書籍『プログラム書法』(The Elements of Programming Style)である。古典であるがゆえにプログラミング言語も古く、例がもっぱらFORTRANであるため言語の設計の古さによる制限に由来する記述も多いが、本質(Elements)は不変・普遍である。
なお、『プログラミング作法』は "The Practice of Programming" という書籍の邦題、『ソフトウェア作法』は "Software Tools" という書籍の邦題である。
あるプロダクトに見られるスタイルは、単にその作者の好みの問題という場合もあるし、何らかの「コーディング標準」や「コーディング規約」などと呼ばれるものによるものという場合もある。PythonのPEP-8、PHPのPEARのように、言語のメンテナらによる、標準的なガイドラインがある言語もある。
よいスタイルを明確に定めるのは困難である。しかし、多くの合意が得られるであろう事柄はいくつか存在する。
演算子やキーワード(予約語)の前後での空白の使用
ユーザー定義識別子(関数名・変数名など)の選び方、作り方
適切な制御構造の使用、自由度が高すぎる機能を乱雑に使用することの戒め(例えばgoto文)
などである。
いずれにせよ、良いスタイルが志向するのはプログラムの明瞭な表現である。従ってどんな言語のどんな流儀においても、
といったような、基本的な理念が共有される。
ここでは主にソースコードの見た目を扱う。ソースコードの見た目は、経験の違いなどによる主観も入ってくるが、概ねメンテナンス性や可読性(読みやすさ)に影響する。
見た目に含まれるうち、フォーマットに関するものは、ガイドラインではなくルールとして、何らかのプログラムによる自動整形に任せてしまう場合もある。その場合、命名法などが残りの関心事となる。プログラムにソースコードのフォーマットを任せてしまうのは、時間の節約の他、ある程度より開発の規模が大きい場合の統一が極めて容易という利点もある。ただし、理由があって変則的なコードの場合に回避できるか、して良いか、など、議論が残る可能性は皆無ではない。またそのフォーマットを実行するツール自体の出来が悪いと、逆に問題になりうる(バグがある、カスタマイズ性が不十分など)。
また、以下の規則類の上位規則として、例えば「規則に従うと極端に行が長くなる」などといった理由が無い限りは、複数の流儀があるものについてはどの流儀を選んでもよいが、基本的に、一貫した書き方にせよ、というものがある。
太古のFORTRANでは空白類は、ほとんど全ての場合にあっても無くても同じであり、80桁のパンチカードに適切に並べることが重要だった。その後の言語では、「そこに空白が無ければ、トークンが連結してしまう」という場合には意味があるが、複数個の空白による位置の調整には特に意味が無く、また、改行も通常の空白と同様の扱い、という言語が増えた(ただし改行には、1行コメントの終了という重要な意味があることもある)。
積極的にインデントを利用するオフサイドルールを採用している言語もあるが、それらについてはここでは触れない(オフサイドルールの記事を参照)。
以下では、広義の空白類の扱いに関係する、各種のスタイルの話題を述べる。
字下げスタイルは、制御構造やブロック(複文)を識別し易くする。PascalやC言語のように、構文規則上、begin 〜 end のキーワードや、波括弧{}でブロックの範囲が決まるフリーフォーマットの言語では、字下げスタイルは意味には無関係であるため、純粋に見た目の問題である。しかし一貫した論理的な見た目により、コードは読みやすくなる。次のコードを比較してみよう。
上の2つの例の方が最後の例よりも読みやすいと感じる人が多い。字下げスタイルは複数の構造が入れ子になっている場合に特に重要となる。
また、上記の例は
とも書けるし、
とも書ける。
どのスタイルが読みやすいかは、いくつかの評価基準があり優劣を付けられるものではないが、同じプログラム(少なくとも同じソースファイル)に複数のスタイルが混在するのは好ましくないことは自明であろう。
隣接する行の桁位置を合わせると、誤字を見つけやすくなることがある。例えば次のコードを比較してみよう。
後者の例では、前者の例で必ずしも明らかでなかった次の2点が明らかとなっている。
search と replacement は何らかの関連があり、対応している。これらは別個の無関係な変数ではない。
search の方が項目が1つ多い。これがバグなら、より目立つようになっている。
言語によっては桁位置合わせで関連性を示すよりも、構造体などで明確に関連性を示したほうが良い。また、型がある言語では型と変数名が遠くなることから、その他コードに修正を加えた場合、関係の無い行にも修正が必要となるため、逆に悪いスタイルとされることも多い。あるいは、単に面倒ということもあり、実施されないこともある。
あるいはアサーションを使い、2つの配列の要素数が同じでなければならないというような前提条件を満たさなかった場合、プログラムを強制的に終了させてしまう検査用のコードを入れることもある(契約プログラミング)。
行の途中でのタブを禁止することも多い。なぜならテキストエディタの種類や設定によって、タブ文字をどういう幅で表示するかが異なるためである。もっとも、等幅フォントを使用しない前提では、タブ幅はもはやインデントレベル以外の意味を持たなくなるため、この限りではない。これは言語による所も大きく、例えばアセンブリ言語の場合はラベルやニーモニックがたいていはかなり短い、という前提で、行の2番目や3番目のトークンとなるオペランドも、タブで位置を揃えることも多い。
ASCIIで 0x0C、あるいは「^L」などと表現される改頁のコードは、2023-02のプログラミング言語処理系ではそもそも空白として扱われず、が、昔、ソースコードをプリントアウトすることが多かった時代には、大きな区切りとして次の部分を紙の先頭から始めたい場合に改行と併用して使ったもので、2023-02でもたまに古いソースコードで見掛けることもある。エディタの言語モードや、ソースコードをPostScriptやHTMLで読みやすく変換するツールなどが、これに対応していることもある。
次のC言語のコードを比較してみよう。
この2つのコードは、どちらがより良い表現と言えるのかはかなり個人差が大きい。空白を多めに取るスタイルでは、演算子の関係などは明瞭になるものの、一つの行としてのまとまりを欠く。「地の行では空白を入れるが、括弧内は詰める」といった折衷的なスタイルなどもあり得る。
「変数名の適切な選択」が指導原理と言えよう。不適切な変数名はコードを読みにくくし、理解しにくくする。
例えば次の擬似コードを見てみよう。
get a b c
if a < 24 and b < 60 and c < 60
return true
else
return false
変数名の選択方法がよくないため、この関数のコードは何をしているのか理解しにくい。しかし次のように変数名を設定すると、より分かりやすくなる。
get hours minutes seconds
if hours < 24 and minutes < 60 and seconds < 60
return true
else
return false
コードの意味が分かりやすくなった。すなわち「与えられた時刻情報が24時間制に合っているなら true、合っていないなら false を返す」である。
適切な変数名の選択は、プログラム全体で一貫性を持った命名を行うことで、さらにその効果を高める。
プログラマによっては、上のようなブーリアン型の計算結果と判断が単純に対応した判断構造は、冗長すぎるし間違いやすいと考える。次のように、そのまま直接、論理式で表現してしまうべきと考えることもある。
これらの違いは意味には影響しない。2023-02のコンパイラはどちらも同じオブジェクトコードを生成する。
逆に前者のスタイルを好む場合もある。その理由としてデバッグの容易さ等が挙げられる。ブーリアン型の条件式の中で変数への代入も行われる場合、その計算後に変数の値をデバッガで確認したいとすると、前者の方が確認が容易である。後者では、その行の実行完了後に停止させると関数から抜けてしまっており、値を確認できない(命令単位のステップ実行なら確認可能)。特に、この例には含まれていないが、条件判断に関数呼び出しが入る場合など、カバレッジを見るにも、文として分かれていたほうが扱いやすいと感じる場合がある。
ループなどの制御構造についても、スタイルに関する議論がある。例えば、次の擬似コードを見てみよう。
count = 0
while count < 5
print count * 2
count = count + 1
endwhile
このコードは変数名や字下げは問題ないが、次のようにfor文を使った方がずっと読みやすい。
for count = 0, count < 5, count = count + 1
print count * 2
多くの言語では、このようなパターンは次のように短縮できる。
for count = 0 to 5
print count * 2
print "Ended loop";
ブロック(複文)のある言語で、制御構造の本体が「文、または、ブロック」である場合、ブロックにすることを原則とすることがある(PerlやGoなど、言語自身の構文規則でこのように強制しているものも多い)。
for (count = 0 to 5) {
print count * 2;
}
print "Ended loop";
これは例えば、次のような「誤ってセミコロンを付けた」発見に時間のかかるバグを回避する。
for (count = 0 to 5);  // この行の最後のセミコロンがバグ
print count * 2;
print "Ended loop";
あるいは、次のようにループ内で実行する行を増やしたときも中括弧を予めつけておけば、バグを回避できる。
for (count = 0 to 5)
log "loop reached " + count;
print count * 2;  // この行が「実はループ内ではない」のがバグ
print "Ended loop";
さらに、「プリプロセッサによって本体が削除されてしまい、意図していなかった次の行が本体になってしまった」というような場合も防げる。
for (count = 0 to 5)
print "Ended loop";