ジャトミカ学習帳

プログラミングとかデジタル信号処理とかのメモ書き

学習帳の表紙

1991年生まれ、2016年3月に大学院卒、4月に新社会人となったプログラマーです。音声信号処理の仕事をしています。

入社前は他人と共同でコードを書く経験がなかったため、特に書き方にこだわることなく散らかり放題のコードになっていました。 入社して他の方に自分のコードを見られる環境になり、自然と「綺麗な分かりやすいコードを書きたい」という感情が膨らんできました。

「いかに美しいコードを書くか」を追求するため、自分の思考整理場としてこのブログを始めました。拙い内容かもしれませんが、スキルアップしていければと思います。

会社ではC++と社内言語を主に使用しているので、ブログ内ではC++を用いたコード例が大半になるかと思います。 ただし、なるべく利用言語に囚われない形で説明していく予定です。

プログラマーにとって以前のコードをリファクタリングするのが当然のように、ブロガーにとっても過去記事のメンテナンスは必要だと思っています。
誤り等ありましたらご指摘いただけると幸いです。

不必要なコメント・必要なコメント(2/2)

気付いたら前回の更新から3か月も経っていますが、今回は「必要なコメント」を題材に書いてみたいと思います。 忙しすぎてブログを書く心の余裕がなかったんですが、ようやく落ち着いたので。いつもいつも忙しいって言っている気がするなあ。

必要なコメント一覧

疑問点を残しておくコメント

他人のコードを途中から自分が引き継ぐことになったんですが、ところどころ制御の怪しいコードが見受けられたので疑問に思った内容をコメントで残しておきました。ちょうどその時、前の実装者が休み期間だったので...。後日その方が出社して僕のコメントを見て、間違っていたところは修正する,意図的だったところは解説コメントに書き換える,という更新がされていました。後で聞けばいいやだと忘れがちなので、自分の場合こういうケースはコードに直接書いてしまいます。どうせ開発終了時には消えるであろう一時的なコメントであれば躊躇う必要はないと思います。

あとで手を付けます系コメント

"tommy TODO" のように未実装であることを明示しておくもの。関数は用意したけど仕様未定のため関数の中は空っぽとかの時にこれを書いておきます。開発初期に多いので結局これも開発終了時には消えます。未実装部分が残ったままになっていないかの検索ワードにもなります。でもTODOはいろんな人が書いていたりするので、検索ワードとして被る可能性が大です。ある先輩は被らないように XXX と書いていたそうですが、なぜか他の実装者がそれを見習って XXX と書くようになり結局被った、という話もあります。笑

絵で示すコメント

百聞は一見に如かずに近いですが、言葉で説明されるより目で見た方が分かりやすい場合がプログラミングでもあります。音声信号処理をしていると回路の制御をプログラムすることが多いんですが、ちょっとした回路図をコメントで添えておくと途端に分かりやすくなったりします。当然大きな図になる場合は別ファイルに描きます。

リファクタリングしたいけどなあ...系コメント

汚いコードを見るとどうしてもリファクタリングしたくなって精神が乱れます。(僕だけ?)コードをいじるほどの時間の余裕はないから放置するけどこのままではいいと思ってないよと明示するために、本来はここをこう変えるべきというコメントを残しておくことがあります。他人のコードを引き継いだ時が大半。プライド高い人におすすめ。

理解に時間がかかった時の解説コメント

次の人がすんなり理解できるように。コードのアルゴリズムそのものが改善できる場合も多し。

まとめ

前回の「不必要なコメント」
不必要なコメント・必要なコメント(1/2) - ジャトミカ学習帳
で書いたことほど取り上げるものはないなあという印象。まあ要らないものがはっきりすれば要るものも分かりますからね。どちらかというと不必要なものを削ることの方が自分の中でプライオリティーは高いです。 でも今回の「絵で示すコメント」とか、普段なかなかやらないであろうコメントもいざやってみると新たな世界が開ける気分になります。

不必要なコメント・必要なコメント(1/2)

コード内のコメントについてはプログラミングにおいて最も議論されやすい話題ですね。人によって意見が食い違う部分も多いですが、適切なコメントを付けられるかどうかで開発効率は圧倒的に変わるのは間違いありません。

今回の記事ではまず「不必要なコメント」について取り上げたいと思います。 不必要であるがゆえに開発途中に見かける例も少ないので、あくまでも初心者・中級者向けです。

不必要なコメント一覧

コードの処理をそのまま翻訳したコメント

if文の手前に書いてある「if文による条件分岐」系のコメント。見ればすぐに分かります。 新入社員の子がこういう類のコメントを書いていたのを見ましたが、プログラミング初心者は不安になって書きがちです。でも自分でそう書いたということは理解しているから書けるのであり、結局誰の助けにもなっていません。消しましょう。 自分も昔だったら「コメントがあって丁寧なコードだなあ」と思っていた気がします。入門用の本には説明として書かれていることがあるので、それを無意識に模範としてしまうのかもしれないですね。

バグがあるのでこの関数は使わないで系コメント

これも新入社員の子が書いていました。全員が全員そのファイルを見ているわけではないです。 アサート処理を入れておくとか関数自体をコメントアウトして使えなくしてしまうとかの方が安全です。

すぐにコードが改善できるコメント

funcAの横にある「〇〇を計算する関数」系のコメント。コメントで説明をするのではなく、funcAという関数名を変えて実行されるコードそのもので説明すべきです。 ただし時間がなくて急遽手当てした場合など、リファクタリングにまで手が回らないケースもあります。そういう時は後でこっそり修正しましょう。

長すぎる説明コメント

あまりに説明が長くなりそうな場合はテキストファイルなどに書き、そちらを参照するようにコメントしておきましょう。分かっている人にとってはそもそも読まなくていいですし。 また長すぎる説明が必要になること自体も疑った方がいいです。別のロジックでよりシンプルなコードにできるかもしれません。

難しすぎる説明コメント

このコメントはいったいどう意味なんだ...と何ヶ月経っても分からないコメントがあります。そんなに悩むということはそのコメントが役割を果たしていないことになります。 そんなことで頭を抱えるくらいなら、思い切って消してしまってもいいかもしれません(断定はしない)。あと頑張って英語でコメントを残そうという習慣があったりなかったりしますが、英語で書いた故に何が言いたいのか分からなくなってしまっては本末転倒です。

過去の実行コードを取っておくコメント

理解しきれていないので消すのが怖い、仕様が定まり切っていないのですぐに変更が効くように、など理由は様々です。 開発途中であれば残しておいてなんら問題はないのですが、開発が終わった後もこの類のコメントが残っていることがあります。 これが積み重なると、どんどんと見にくいコードになっていってしまいます。 理解しきれずに不安なまま変更を加える必要がある場合は非常にバグを仕込みやすいので、できる限り理解することが先決です。 初心者でなくてもやりがちなNo.1の例だと思います。

履歴コメント

「〇月〇日にこういう変更を加えました」系のコメント。これは初心者どうこうではなくコーディングルールの問題ですね。昔は必要だったのかもしれないですが、今はファイル管理システムを使うのが当たり前なのでそのコミットログを見ればよいだけです。そもそもコミットログの方が差分も見れて信頼できます。 ファイル管理システムを使っていない場合はそれ自体が問題と認識すべきです。エクセルファイルのように簡単に差分が見れないファイルだったらありかも。

コメントが不必要な理由一覧

コメントの種類ではなく理由という切り口でも分類してみましょう。内容は上と被ります。

コードの量は少ない方が分かりやすいから

1000行のコードを読み解くより、100行のコードを読み解く方が簡単です。 すなわちもしコメントが理解の助けに全くなっていない場合はない方が行数が減ってマシです。

コメントそのものにもコストがかかるから

「コメントに目を奪われる時間」「コメントを理解する時間」「コメントの信頼性を確かめる時間」など、意外とコメントそのものにコストがかかってしまうものです。

嘘である可能性があるから

コメントが書かれた後に実行コードの修正が入るとコメントの内容が嘘になってしまいます。後々のバグに繋がります。 コメントは実行コードではないとは言え、”実行コード予備軍”くらいの影響力はあります。

メンテナンスが必要だから

コメントも随時修正必要だなあ....面倒だなあ、と思う場面は多いです。たまにしか見ないファイルなら許せますが、頻繁に手を加えるファイルなら本当にそのコメントは必要か?と吟味しましょう。

リファクタリングが可能だから

分かりやすいコード>分かりにくいコード+コメントによる説明 とよく言われます。

まとめ

コメントを書くよりコメント消す方が簡単に思えますが、「不適切なコメントだ」と判断するのは意外と難しいです。なので、そう判断できたのなら後々他の方が同じ苦労をしないように消してしまうのが吉です。 要らないコメントを消してコードをスッキリさせたら、今度は適切なコメントを加えていくフェーズです。

次回は必要なコメントについて記事を書く予定です。

プログラムの複雑さはロジック側ではなくデータ側に寄せる

「プリンシプルオブプログラミング」をざっと読みました。
定期的に読み返してじっくり自分の中に植え付けていきたい、と思える内容でした。
為になった点をピックアップし考察していきます。

表現性の原則

UNIX思想の一つで「情報はデータに寄せて表現」ということらしいです。理由を見てみると、そちらの方が容易に理解できるから、とのこと。
UNIXには抵抗感がありましたがこの本を読んでからだいぶ消えました...(^^;

例を挙げて考えてみます。以下のfuncAはどんな処理をしているのでしょうか。

int funcA(int value){
        if(value<=1) return 1;
        return funcA(value-1)+funcA(value-2);
}

関数名はあえて分かりにくい名前にしています。
ピンと来る人は一瞬でしょうが、そうでない人は理解するのに数分かかってもおかしくありません。

さて次の場合はどうでしょうか。

static const int table[10] = {1, 1, 2, 3, 5, 8, 13, 21, 34, 55};
int funcB(int value){
        return table[value];
}

こちらの場合は見ただけでパっと気が付く人も多いと思います。フィボナッチ数列ですね。 人間はロジックを追うよりもデータを見た方が理解にかかる時間が短くなるわけです。

このように「どちらの記述を採用しようかな?」と思う場面は多々あります。

ロジックが2つある場合どちらシンプルにするか

デジタル信号処理の仕事をしていると頻繁に遭遇するのが、デジタル回路の構成もしくはその制御をしているプログラムのどちらのシンプルさを保つか、という問題です。 言い換えると、「回路のロジック」と「制御のロジック」のシンプルさの選択です。

例えば、「複数の入力信号から一つ選択し出力するスイッチ制御」を考えてみます。
なるべく省資源で抑えたいケチル君と資源を気にしないムダヅカさんの2人に回路設計を頼みました。2人の作品は以下です。

f:id:tommy_dsp:20171202220459p:plain

ケチルくんの回路はスイッチ1つのみで構成されており、回路を見ただけでどちらか一つの入力のみを使うことが分かるので完璧です。 ムダヅカさんの回路はスイッチを2つ使ってしまったため、「もしかしたら入力1と入力2が両方出力される場合もあるのかな?」と、回路を見ただけでは分かりません。 お題は「複数の入力信号から一つ選択し出力する」ことであるので、この回路構成は間違いではないですが不親切です。

では、入力信号が4つの場合はどうでしょう?再度2人に設計を頼みました。

f:id:tommy_dsp:20171202205629p:plain

ケチルくんの考え方は、1~4までの数字が2進数で00, 01, 10, 11 と2bitで表現できるのと同じように、4種類の入力も2種類のスイッチがあれば切り替えられるだろうというものです。 ムダヅカさんはスイッチの種類や量を気にすることなく入力に対して別々に用意しました。

ここらへんから何となくムダヅカさんの回路の方がパッと見て綺麗に感じませんか? 確かにケチルくんの回路は合理的で制御する際にも誤った動作になることもなく、資源も少なく済んでいます。ムダヅカさんの回路は上手くスイッチの制御をしないと出力信号が4倍になるケースもあり得ますが、回路の見た目としては非常にシンプルです。

さて、この2人の回路をあなたが引き継ぐことになりました。そして上の人から「入力5つに増えたから仕様変更よろしく」と言われたとします。 5つ目の入力用経路を組み込みやすいのはどちらの回路でしょう?
説明するまでもなさそうですが貼ってみます。

f:id:tommy_dsp:20171202211423p:plain

ムダヅカさんの回路を引き継いだなら「はーい」と二つ返事で受け入れてこう再設計するでしょう。5つの入力が並列であることが一目で分かります。 ケチルくんの回路を引き継いだ場合は多少モヤモヤ感があり、苦し紛れに最後の部分にプラスアルファした形跡が残ります。 後から見た人は、入力5だけなんか特別扱いしてる?と変な想像をしてしまいます。スイッチだけ見てもAに比べB, Bに比べCが強い力を持っており粒度が揃っていません。
2つの切り替えの時はケチルくんの考え方のほうが、機能拡張していくことが想定されるならムダヅカさんの考え方のほうが回路・制御を含めた全体のロジックとしてシンプルなんですね。

このように2つ以上のロジックのどちらをシンプルにするか迫られた場合は、全体を俯瞰して考えてみることが大事だと思います。 もちろん制御の処理スピードを優先したいなどの条件があれば正解は定まりますが、基本的にはなんらかのトレードオフの関係になることが多いです。

ちなみに今回の話は「最適化を求めると運用・保守が難しくなる」という原理にも通じていると思います。

おわり。

Kindle版「プリンシプルオブプログラミング」を購入

寝る前に本を読むことが多く、部屋が暗い状態でも読める電子書籍は便利だなあと思ったので、Kindle版を購入。 また汚れを気にせず大事な文章にマーカーを引き放題なのも便利だなと感じました。
デメリットしてはやはりページ遷移の面で読み辛さは否めない点と、目に悪そうな点です。Kindle話はこのくらいに。

リファクタリング」「リーダブルコード」のどちらも為になったのでその他いろいろな名著を買いたい衝動に駆られているのですが、たくさんの名著の橋渡し的役割を果たしている本書を次の本に選びました。 様々な名著から重要な点をピックアップし、普遍的事実を紹介しているような感じです。先に読んだ2つも参考書籍として何回か登場しています。

本書の面白いところはただひたすらに抽象的概念で説明が進んでいくところで、具体的なコード例などはほとんどありません。 そのおかげで読者の利用言語に縛られずに誰にでも受け入れられやすくなっている点は流石だなと思います。 リーダブルコードは様々なプログラミング言語を用いた豊富なコード例のおかげで読みやすくなっていますが、本書は完全に逆方向に振り切っています。こんな書き方もあるのだなと。

ブログを書いているプログラマーの多くは自分の得意言語でコード例を載せていると思われますが、PHPとかJavaScriptとか、自分にとって馴染みのない言語で書かれていると若干読む気が失せてしまいます。 このブログもひたすらC++で書いていく予定だったのでウッ!ヤラレタ!という気分ですが...(^^;) より抽象的な説明でブログを書けたら面白いかも、と思いました。

昨日買ったばかりなので、具体的な内容についてはもう少し読み砕いてからにします。

嫌がられない三項演算子の書き方

「使うべき」派と「使わないべき」派で意見が分かれる代表格とも言えそうな三項演算子。 自分も学生の頃は使わない方が良いんだろうなあと思って避けていて、入社後1行でスッキリ書けるエレガントさにはまってしまってバンバン使うようになり、リーダブルコードを読んだ後はどっちが良いんだ?となり...
この派閥を行ったり来たりしていました。 なんとなく自分の意見がまとまったので、記事として残しておきます。

メリット
・コード量が減りスッキリ書ける

デメリット
デバッグがしにくくなる
・理解し辛くなる危険性がある
・馴染みのない人がいる

つまり、なぜ三項演算子が嫌われがちなのか推測すると、
肯定派の中には「三項演算子を使いこなせる俺カッコイイ!」
否定派の中には「この "?" ってどういう処理なんだっけ...」
と、どちらの派閥にも頼りないプログラマーが存在するからだと思います。

とある先輩がこれどっちがどっちだっけ、と聞いてきたときには流石にその人に対する信用を失ってしまいました。 自分が使わないにしてもせめて読めるようにはなっていて欲しいと思います。

まず前者のタイプのプログラマーが書きがちなコード例について書いてみます。 基本的にはコードは短い方が読みやすいはずですが、無理矢理に行数を減らそうとして逆に読みにくくなったパターンです。

int getEntranceFee (int age, int gender){
    return (age>=20) ? ((gender==MALE) ? 2000 : 1500) : 1000;
}

ネストが深くなっているのも関係して読み辛くなっています。以下のように書いた方がすんなり理解できるはずです。

int getEntranceFee (int age, int gender){
    if(age<20) return 1000;
    else return (gender==MALE) ? 2000 : 1500;
}

しかしこう書いたとしても、後者のプログラマーにとっては「結局成人男性だと2000円?1500円?どっちだったっけ?」となります。 知っている人にはこれで全然問題ないはずですが...もう少し読み手に易しくしたいところです。そこで、

#define MALE_FEE 2000
#define FEMALE_FEE 1500
#define CHILD_FEE 1000

int getEntranceFee (int age, int gender){
    if(age<20) return CHILD_FEE;
    else return (gender==MALE) ? MALE_FEE : FEMALE_FEE;
}

こう書いてあれば三項演算子部分を見て理解に苦しむ人はほとんどいないでしょう。

いやいや!逆に行数増えてるじゃん!とツッコミが入りそうですが、そもそも入場料は経済の影響で変動する可能性があるので、関数の外で定義しておく方が自然。
あくまで言いたかったのは三項演算子部分でどちらの値が入るかすんなり理解できる必要があるということです。つまり以下の場合はNGです。

#define FEE_1 2000
#define FEE_2 1500
#define FEE_3 1000

int getEntranceFee (int age, int gender){
    if(age<20) return FEE_3;
    else return (gender==MALE) ? FEE_1 : FEE_2;
}

別の例でもう少し。

int selling_price = (hasCouponTicket) ? price*0.8 : price; // OK, 「クーポンチケットは割引するもの」という共通認識あり
int seat = (hasTicket) ? seat_A : seat_B;                  // NG, 三項演算子知らない人はどちらか迷う
int seat = (hasVipTicket) ? vip_seat : general_seat;       // OK, 変数名のおかげで分かりやすくなった
int value = (a<b) ? a : b;                                 // NG
int min = (a<b) ? a : b;                                   // OK

変数名のつけ方大事だね、という話になってしまった気がしますが、おそらくリファクタリングの概念というのは互いに密接に関わっていると思われます。 こう書けない場合やデバッグのしやすさに影響が出る場合には、無理に三項演算子にする必要はないです。

なお、三項演算子の条件式の部分に括弧を付ける必要はないですが、if文の場合は自然と条件式に括弧が付くので、それに合わせて付けておいた方が個人的には読みやすくなる気がします。(好みの問題)

「ガード節」をいつ使うか

名著と言われている「リーダブルコード」を読んでいるので、気になった項目をピックアップしながら自分なりの考察を加えていこうと思います。 基本的に本の内容をそのまま引用しないようにしています。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

ガード節とは

関数の先頭で例外をはじく処理。ガード節という言葉がリーダブルコードで命名されたものなのか、プログラミングにおける一般的な用語なのかは、よく知りません。

1. あり得ない値をガードする
void calcArea(float width, float depth){
    if(width < 0 || depth < 0) return; /* ガード節 */
    ... /* メインの処理 */
}

関数によっては利用可能な引数の範囲が制限されていたりします。
たとえば100個のデータセットしかない配列で引数に応じて配列の参照位置を変える場合は、「0から99までの引数で呼ばれること前提」になります。
関数を呼ぶ側で適切な手当てがあれば問題ないのですが、そちらの実装担当が別の方だった場合は上手く連携を取れていないとバグになり得ます。

ではこのガード節を使えば解決、という簡単な話でもなく、問題点がいくつかあります。

  1. コードの見栄えが悪くなる
  2. ガードしたことに気付かない
  3. 処理が(多少)遅くなる

上記のコード例でいう ”メインの処理” の部分が1行で書ける関数の場合、ガード節を挟んでいくだけで処理の行数が2倍に膨らんでいきます。 規模の小さな関数にも全てガード節を挟んでいったら、見栄えはかなり悪くなってしまいます。

2点目について、ただ単にreturnするだけでは実際にバグが出ていても見落としてしまいます。 何かエラーメッセージを表示するなどでエラーを明示的に表示すれば見落としませんが、そうすると1点目の問題と同じでますます見栄えは悪くなっていきます。

周辺の関数とは明らかに引数の範囲が異なる場合など、「なんとなくバグが出そう」というところに挟んでいくのが実用上良さそうです。 また実行速度を優先する場合、ガードされることなくテストにクリアしたら製品リリース前にガード節を取っ払ってしまっても構わないでしょう。

2. 必要な情報が揃うまでガードする
ClassA(){ // コンストラクタ
    this->mWidth = -1;
    this->mDepth = -1;
}

void ClassA::setWidth(float width){
    this->mWidth = width; // メンバ変数へ保持
    calcArea();
}
void ClassA::setDepth(float depth){
    this->mDepth = depth; // メンバ変数へ保持
    calcArea();
}
void ClassA::calcArea(void){
    if(this->mWidth < 0 || this->mDepth < 0) return; /* ガード節 */
    ... /* メインの処理 */
}

こちらのケースは非常に有効的です。
何度も似たような計算を繰り返すことなく、必要な情報が全て揃ってから1回のみ計算するだけなので、メインの処理の負荷が重ければ重いほど、もしくは必要な情報が多ければ多いほど効果を発揮します。
また、テスト後に取っ払うわけではなくリリース後にも当然残る処理です。