ソースコードの分かりやすさって何だろう

結論はない。

背景

最近またGaucheを勉強し始めているのだが、ドキュメントを読むとdatumという表現が良く出てくる。

Special Form: case key clause1 clause2 …
[R7RS][SRFI-87] keyは任意の式です。clauseは以下の形式でなければなりません。

((datum ...) expr expr2 …)
((datum ...) => proc)

http://practical-scheme.net/gauche/man/gauche-refj/Tiao-Jian-Shi-.html#g_t_6761_4ef6_5f0f

datumって何だろうという疑問を持ち、調べると「dataの単数形」だと知る。 Lisp/Scheme界隈では一般的っぽいけど、こっちの世界に詳しくない僕には聞き慣れない言葉だった。 (他にも関数型では一般的?)

一瞬「datumって分かりにくいな」と思ったのだけど、上記の通り一般的な用語なので単純に僕の知識レベルに要因があるだけだとすぐに思い直し、その勢いで最近感じているコードの可読性についてポエムを書いている。

プログラミングにおいてソースコードは書くより読むことの方が多いはずで、チーム開発をしていれば自分が書いたコードより他人が書いたコードを読むことの方が多い。 将来の自分は他人ということであれば、自分が書いたコードですら時間が経てば他人のコードになる。 必然、コードは(パフォーマンス上の理由がなければ)人が読みやすいように書くべきだ。

では「分かりやすさ」とは何だろう。というのをぼんやり考えることがある。 「面白い」「美味しい」と同じように、当人の経験やスキルや価値観に大きく影響するものであり、普遍的な「分かりやすさ」などないのではないか。

そもそも職業プログラマたるもの、わざと分かりにくいコードを書く人などいるのだろうか。どんなコードであれ本人にとってはそれは分かりやすいのではないか。本人が分かりやすいと思っている以上、周りが分かりにくいと伝えてもピンと来ないのではないか。

ソースコードについて

まずコードレベルの細かいところから言えば、例えば条件分岐1つとっても色々な書き方がある。 (以降の例は擬似コードだがハイライトのためにcode syntaxは一応javaで指定している)

// パターンA
function something(type) {
    if (type == "a") {
        return "Aだよ";
    } else if (type == "b") {
        return "Bだよ";
    } else {
        return "なんだろうね";
    }
}
// パターンB
function something(type) {
    var result = "なんだろうね";
    if (type == "a") {
        result = "Aだよ";
    } else if (type == "b") {
        result = "Bだよ";
    }
    return result;
}
// パターンC
function something(type) {
    var type_patterns = [
        "a" => "Aだよ",
        "b" => "Bだよ",
    ];

    // 言語によっては存在しない場合のデフォルト値を指定できるが一応愚直に
    var result = type_patterns[type];
    if (result is not null) {
        return result;
    } else {
        return "なんだろうね";
    }
}

焦点はif文なので関数名、変数名の適当さは無視する。

僕は大概パターンAかCで書く。ロジックを分けたい場合はAで書き、(結果の)データを分けたい場合はCで書くことが多い。が他の判断基準もあるし、最近では後述するように基本的にはチームに合わせる。 ちなみに僕は個人的な好みで言えば最後のelseは書かない方が好きだし、1回だけでシンプルなものであれば条件演算子を使うのが好きだ。

// パターンC
function something(type) {
    var type_patterns = [
        "a" => "Aだよ",
        "b" => "Bだよ",
    ];

    var result = type_patterns[type];
    return (result is not null) ? result : "なんだろうね";
}

条件分岐の例を出したが、コードレベルの流派は他にも色々ある。 ループで書くかmap,each,foldみたいなコレクション操作で書くかとか、状態をクラス変数に持って取り回すか引数でもらうようにするかとか。

どこまでメソッドを切るか、というのもコードレベルの話と言えるかもしれない。 以前は1メソッドは短いほどいいと思っていたけど、メソッド呼び出しがあちこち飛ぶと分かりにくいし、OSSとか見ていると長いけど読みやすいコードもあると知った。当然長くて汚い最悪のコードもある。

OSSの場合はパフォーマンスとかの問題でメソッドを分けていない可能性もあるが、とにかくメソッドの行数と可読性は必ずしも一致しないことを学んできた。もちろん適切にメソッドが分かれていれば分かりやすい。が、それに失敗したメソッドは、愚直に長いメソッドより分かりにくいこともあるのではないかという気がしている。 とはいえ個人的には、それでも長いメソッドの中で処理のコメントを書くならそれをメソッドに切ったらいいのではと思うけど、それは僕の「分かりやすさ」であって、万人の「分かりやすさ」ではないのだと思う。

好みといえばフォーマットが典型的だ。言語によってこれだけ規則が違うのは好みの千差万別さを表している。好き嫌いを廃止して(構文が許す限り)全ての言語でフォーマットが統一されていれば各種ツールとかの再発明も必要ないだろうがそうはいかない。 コードを書く上で気持ちよさは大事だし、フォーマットは気持ちよさに影響するのだ。

Goのように公式でツールがサポートされていなくても、最近はほとんどの言語でフォーマットの規約があるしサポートツールもある。問題はプロジェクトがそれに準拠しているかどうかだ。 イメージというか偏見かもしれないが、スタートアップの場合は「大事なのは分かるけど整備している暇があったらプロダクトコード書く」という感じで後回しにされ、大規模とかSIだとそこまで重要視されていないように思う。 ということで、フォーマット規約をきちんとルール化しているプロジェクトにはあまり出会ったことがない。SIerの頃にJava案件でEclipseのフォーマット設定が共有化されていたような記憶はぼんやりある。

他には命名規則にも好みは表れる。例えば何かの配列を表す場合、僕はnameListよりはnames(複数形)を好む。でも人によってはListにした方が(多少文字数は増えても)パッと見て分かりやすいだろう。namesだともしかしたらnameと読み間違えるかもしれない。静的型付けだったらそんなミスは起こらないかもしれないが。 また昔はgetXXXとしていたが最近では単にXXXでも別に良いと思うようになった。呼び出している側で代入していれば戻り値を返すのは明らかだ。

アーキテクチャや開発手法について

トランザクションスクリプトが好きな人がいればそうじゃない人もいる。OOPやDDDが好きな人もいればそうじゃない人もいる。UMLで設計する人もいれば口頭やplain textだけの設計で終える人もいる。 アジャイルウォーターフォールといった開発手法もそう。

ここではアーキテクチャ/手法の良し悪しではなくて、あくまでチームメンバーにとって「分かりやすさ」は違うという観点で話している。 どれだけそのアーキテクチャが理論的に優れていても、正しくても、実行・メンテする人が理解していなければ負債でしかない。 難しいアーキテクチャを採用すべきではないということではなく、チームメンバーによって最適解は変わるのではないかという話。

個人的にはちょっと前に読んだ「ユースケース駆動開発実践ガイド」が面白かったのでこれで開発してみたいけど、自分が長期的に開発していくプロジェクトでなければ選択することはまずないだろう。

ドキュメント(コメント)について

プログラミング覚えたての頃はコメントがないと辛かった。知人に「コード読むのに邪魔だからJavadocを全て消すようなプログラム書いた」という人がいてその感覚を理解出来なかった。

ある程度プログラミング出来るようになってくると、今度はコメントはいらないと思うようになった。「結局コードを読むから」というのが理由だった。 当時はほとんど業務コードしか読んでいなかったせいもあって、コメントが正しいことを保証するには結局コードを読む必要があり、コメントに意味を感じなかった。

今では「必要かつ最小限のコメントは書くべき」という立ち位置になっている。リーダブルコードでコメントへの考えが変わったのもあるし、少しずつOSSのコードも読むようになってきて確かにコメントが理解の助けになることを感じてきた。 それまではせいぜい「どうしてもコメント書くならHowじゃなくてWhyのみ書け」という思いだったが、それだけじゃなくて例えば使用例などがあっても良い。 結局「理解の助けになる」なら何でもいいのだ。リーダブルコードにそんなことが書いてあったような気がするし書いてなかったかもしれない。忘れた。

最近、数年前の自分のコードをいじる必要があったが構造とか意図とか全然覚えていなくて、億劫ながらコードを開いたらコメントが随所に書いてあって助かった。たぶんリーダブルコード読んだ後でコメントは大事という認識が強いうちに開発したのだと思う。当時の自分を褒めてあげたい。

そんなコメントもまた「理解の助けになる」の基準は人によって異なるという問題がある。ある人にとっては自明で余計なコメントでも、ある人にとってはそれが助けになるかもしれない。

先人の知恵

リーダブルコードやClean Codeといった書籍もあるし、ネット上に「分かりやすいコード」の情報はたくさんある。 それらに共感できるスキル/マインドセットを持った人だけでチームが組まれているなら、それを基準にすればいいかもしれない。

でもたぶん、そんな簡単なことじゃない。 例えばこれは細かい例だけど=の位置を揃えるというのがある。

var name    = "xx";
var age     = 10;
var address = "yyy";

僕は最初こういうコードを見た時「うっ」となったけど、今ではこっちの方が見やすいと思っている。けど昔の僕のようにこれが見辛い人もいる。 または他の細かい例だと、僕は「ifの中身が1行なら{}を書かない」というのが嫌いだ。

...前処理
if(pred)
    return "hoge"
...後処理

一方でrubyなどにあるような後置ifで書くのは好きだ。

...前処理
return "hoge" if pred
...後処理

今の僕はこの通りだが、前述の=揃えのように好みは変わるかもしれない。 今日自分にとって分かりやすいと思って書いたコードが、将来の自分には分かりにくいコードになっているかもしれない。 つまり先人の知恵に沿って分かりやすさを意識したコードを書いても、それが本当に長期に渡って分かりやすい保証はないのではということだ。

理想論では、そうならないように少しずつ日々のコードをリファクタすればいいのだろう。変わっていく感性/知識にコードを追従させればよい。 でも実際は日々の業務は山盛りでそこまで余裕はないし、それは面白い作業ではないし、何より1つのシステムと共に死ぬことはなくて遠くない未来に別のプロジェクトや会社に移動する。

どうするか

大きくはアーキテクチャから小さくはコードのコメントの書き方まで色々あって、人によって分かりやすさは異なる。 となると何を基準に開発するべきか。

ここ数年の僕のスタンスは「今後そのプロジェクトを長期保守するであろう人に合わせる」ことにしている。 まず既存のコードを軽く読み、コードの雰囲気を知る。それから自分の担当部分のコードを書く時は、8割ぐらいはその雰囲気に合わせる。2割ぐらいは自分の好みを混ぜる。 そこでもしレビューで修正依頼が来たとしても、よほどの理由がない限りは指摘事項に合わせる。そして大概の場合、よほどの理由はない。

同様に他の人のコードをレビューする時も、昔は細かく「僕はこう書く方が好み。なぜなら…」みたいなことも書いたが、今ではほとんどしない。

コードをたくさん読むのはメンテ(保守/機能追加)する人だ。メンテする人が一番大事だ。彼らがストレスなく読み書き出来なければ、どんなに自分にとって分かりやすいコードでも意味がない。 「こう書いたらどうですか」と提案することもあるけど最終判断はその人に任せる。

おわりに

書いてから読み直して気付いたけど、「分かりやすさ」と「読みやすさ」を一緒にして考えている。 これらは別々の話ではないかという気もする。一緒かな。

Go言語でWebアプリを作りかけて辞めた話

3行で要約

  • golangでwebアプリを作り始めた
  • golangは好きだけど、今回求める要件に合わないことに気付いた
  • 途中まで書いたコードを捨ててRailsで書き直した

私のGoスキル

背景

とあるtoB向け受発注のWebアプリを作ることにしました。
開発者は私1人。私が目指した基準は以下のものです。

  • 少なくとも3年はメンテできること
  • 未来の自分(他人)が読んでも理解できること
    • このアプリとガッツリ付き合う予定ではなく、出来れば保守は違う人に回したいし機能追加とかも控えめにしたい(重要)
  • デプロイとか運用の手間を簡単にしたい
  • ついでに、学んでおくことが有益になりそうな言語がいい

これらを踏まえ、以下の理由からGoを選択しました。

  • シングルバイナリでデプロイ出来る
  • 大体のことは標準ライブラリで出来るイメージ
  • フォーマットとか強制されるのが良い
  • 静的型付けによる安心感
  • Go書いてみたい

選択したフレームワーク

Webフレームワーク

Revelなどのフルスタックフレームワークは選択肢から外しました。

  • シンプルな受発注(CRUD)なのでフルスタックの必要性を感じなかった
    • これは後に読み間違いと気付く
  • Revelがどこまで活発に開発続くか分からない
    • 開発が止まるor人気がなくなると使える人が少なくなるのでメンテ性が下がる
  • Go使っているのに少なからずDSLを覚えないといけないことに抵抗があった

「なるべくnet/httpの使い方から外れないシンプルなやつ」ということでGinを選びました。
Echoとかも試しましたが好みでGinにしました。

O/Rマッパー

Webフレームワークと同様でDSLを覚えるつもりはありませんでした。
数年後も使われているか分からないライブラリのDSLを覚えるのはきつい。
かつマイグレーションの機能とかも不要で、単純にレコードを構造体にマッピングしてもらえれば十分でした。

gormは色々できるし途中まで使っていたけど辞めました。
gormに限らないが、吐かれるSQLを確認しながら書くのが本末転倒な感じだったので。

これも薄いラッパーということでsqlxを採用しました。

ちなみに以前は「マイグレーション機能っていいな」と思っていましたが、ここ数年で考えが変わりました。
ツールとしてのマイグレーションってそんな必要なのでしょうか。
upについてはalterとかの差分のsqlファイルがあれば十分ですし、downについては今のところ使った記憶がありません。
開発者間/デプロイ環境間でスキーマのversionが異なるような時に便利なのかもしれませんが、そういう開発案件に携わったことがなく。

マイグレーションに限りませんが、昔は「このツール便利!」と歓喜して導入しようとしていたのが今は「便利だけど覚えるコストと負債になり得るリスクを考慮するとどうだろう」と考えるようになってきました。
若手の頃に嫌だった「保守的なエンジニア」になりつつあるのかな…とちょっと心配になります。保守的になると新しい技術を覚える意欲を失いそうで。

ライブラリ管理

glideを採用。
これはちょっと採用理由を忘れました…。

その他

システム上必要なライブラリ(go-sql-driver/mysqlとか)以外は入れていません。
テンプレートも標準のtemplateで済ませています。凝った画面ではないので十分です。

Goで書き始めた感想

良かった

  • 良い
    • ライブラリのコードが読みやすい
      • 階層がほとんどフラットだし、importや呼び出しの名前空間が明示的なのも良い
      • 「この関数どこで定義されているんだ」みたいな余計な手間がない
    • 書いていてストレスがほとんどなかった
    • go fmtなども含めて標準ライブラリ/ツールで済むのが良かった
    • シングルバイナリは良い
    • 静的型付けは良い
    • テストコードが同階層にあるのは意外と読みやすかった
  • 良くない
    • テストコードでassertはやっぱ欲しい
    • エラー処理が面倒

私にとってGoは書きやすいし読みやすいものでした。
「書きやすい」 = 「簡単に書ける」ではありません。
「書きやすい」 = 「書き方に迷わない(迷うことが少ない)」です。

コレクション操作などxx言語ならもっと短く簡潔に書けるのに、というケースはありますが不満は感じませんでした。
簡単に書ける言語だと書き方に迷うことがあるし、レビューでも「こう書いた方がシンプル」みたいな横道にそれがちです。
(今回はレビューする/されることはないので関係ありませんが)

同じ名前空間でもファイルが分かれていることに最初は「うっ」となりましたが、慣れたら気にならなくなりました。

不満としては、テストコードでassert系はやはり欲しいと感じました。
私が手を抜いているのか、テストでは基本的に「AとBが一致しませんでした。Aはこの値でBはこの値です」みたいなメッセージが表示できれば十分です。
これを毎回Errorf(...)で書くのはDRYじゃありませんし単純に面倒です。

また、ここ数年ずっとスクリプト言語で過ごしてきた私にはエラー処理はちょっと面倒でした。慣れだと思います。

そんなわけですごく良い感じにコードを書いていたのですが、ちょっと要件に合わない選択をしたことに気付きました。

GoでWebアプリを作る上で足りていないところ

主にセキュリティ周りの考慮が漏れていました。
CSRFの対策とかid/pass方式の認証とか二重サブミット防止とか。
revelだとcsrfプラグインrevel-csrfというのがあるそうですが、ちゃんと調べていません。二重サブミットとかどうなっているんだろう。

システム要件上id/passの認証が必要なのですがginでは対応していません。
ginも他のフレームワークBasic認証やOAuthは対応しているのですが。
さすがにこれらを自前で実装するのは再発明が過ぎます。

また細かいところでは、例えば「環境変数(production/develop/testとか)ごとに処理を切り替えたい」とかの処理も必要でした。
典型的なところではDBの接続先を切り替えることです。
環境変数を取ってきたりgin.Mode()から取り出したり出来ることは出来るんですが、自分で書いていくと結局オレオレフレームワークへ近付きます。

ここに至り、最初の基準で設けた「シンプルに」「DSLに依存したくない」というのは諦めました。
システムの機能がシンプルでも開発環境や非機能要件では色々必要だった、という見積が抜けていました。
とはいえ私の調査不足なだけで実は解決案は転がっているのかもしれません。

開発初期のスピード感と対応範囲、情報量および開発者の多さから、選ばれたのはRailsでした。
当初の目論見と真逆です。

GoでWebアプリを作りかけた感想まとめ

  • Goは心地よい(not楽しい)
  • (今の自分が)Goで何か作るならバックエンドかバッチか認証や更新操作が必要ないWebアプリ
  • 新しい言語をやる時はやはりその道の先輩がいないと相談できなくて厳しい

Vim - previmにヘッダーを隠す設定を追加しました

プレビュー時に画面上部に出ていたヘッダー部を非表示に出来ます。

f:id:kanno_kanno:20150817221831p:plain

g:previm_show_header                 *g:previm_show_header*
    型:数値

    値が1ならば、プレビュー時にヘッダーを表示します。
    ヘッダーには編集しているファイル名と更新日時が表示されます。

    値が0ならば、プレビュー時にヘッダーを表示しません。
    デフォルトでは1に設定されています。

    " .vimrc
    let g:previm_show_header = 0

.vimrclet g:previm_show_header = 0を追記して頂ければOKです。

Vim - previmでmermaidに対応しました

Shibaで図を書いてTracで共有するを見てmermaidを知りました。
参考: mermaid.jsが素晴らしいけどなかなか使ってる人見かけないので実例晒す(追記あり)

面白そうだし、上記の公式サイトにmarked対応のサンプルもあったのでprevimでも対応しました。

下記のようにコードブロックのタイプをmermaidとしてもらえればOKです。
mermaid自体の具体的な書き方は公式サイトを参考にしてください。
(私も全然分かっていません)

```mermaid
graph TD;
    A-->B;
    A-->C;
    B-->D;
    C-->E;
```

あとは普通のmarkdownと同様にプレビューされます。

Vim - 文字列連結で再代入する場合はjoinを使う方が早そう

概要

自作プラグインのコードに以下のようなTODOが残っていました。

  " TODO リストじゃなくて普通に文字列連結にする(テスト書く)
  for line in s:do_external_parse(a:lines)
    let escaped = 適当な処理
    call add(converted_lines, escaped)
  endfor
  return join(converted_lines, "\\n")

ここでは文字列の配列をぐるぐる回して処理を行い、最終的に改行区切りの文字列としています。
joinで書いたものの文字列連結の方が早いのではと思って上記のようなTODOコメントを残していました。

今日このTODOを消化しようと思い、本当に文字列連結の方が早いのか計測しました。

先に結論

  • 10回程度の連結なら大差ない
  • 3000回ともなると圧倒的にjoinが早い
    • joinの方が実装もシンプル

環境

  • Mac OS X 10.9.5
  • MacVim Custom Version 7.4 (KaoriYa 20150707)

ベンチマーク

h1mesuke/vim-benchmarkを使用します。
テストデータは1行85文字が3000行です。(3000回連結される)

let s:lines = []
for s:n in range(3000)
  call add(s:lines, "    * 注意:拡張子が`.md`の場合は`markdown`ではなく`modula2`として認識されてしまいます。その場合は以下の設定を.vimrcに記述してください")
endfor

let s:bm = benchmark#new("concat string")

function! s:bm.operator_for()
  let result = ""
  let delim  = ""
  for line in s:lines
    let result = result . delim . line
    let delim  = "\\n"
  endfor
endfunction

function! s:bm.operator_while()
  let result = ""
  let delim  = ""
  let n = 0
  let length = len(s:lines)
  while n < length
    let result = result . delim . s:lines[n]
    let delim  = "\\n"
    let n += 1
  endwhile
endfunction

function! s:bm.join()
  let tmp = []
  for line in s:lines
    call add(tmp, line)
  endfor
  let result = join(tmp, "\\n")
endfunction

call s:bm.run(3)

結果は以下の通り。

Benchmark: concat string

Trial #1
  join           : 0.008425
  operator_for   : 0.348224
  operator_while : 0.356583

Trial #2
  join           : 0.010402
  operator_for   : 0.353079
  operator_while : 0.359111

Trial #3
  join           : 0.008463
  operator_while : 0.347276
  operator_for   : 0.359592

joinが圧倒的に早かった。
Java+連結すると遅いように、Vim Scriptでも気をつけた方がいいのかな。

ちなみに10回程度の連結なら大差ない。

Benchmark: concat string

Trial #1
  join           : 0.000046
  operator_for   : 0.000063
  operator_while : 0.000076

Trial #2
  join           : 0.000051
  operator_for   : 0.000080
  operator_while : 0.000083

Trial #3
  join           : 0.000048
  operator_for   : 0.000066
  operator_while : 0.000082

おわりに

「推測するな、計測せよ」の大事さ。