リーダブルコードという本を読む機会があったので読みました。2012年に日本語訳が出版され、今は19刷だそうです。有名な本ですね。
オライリージャパン
売り上げランキング: 576
Nothing new
この本の冒頭に、Amazonで「Nothing New」というレビューが付いたという話が書かれている。それに対して、本書は「面白い」から大丈夫、とある。
私もこの本を読み終えて、たしかに新しいことは何もない(いや、全く無いわけじゃないけど)と感じた。おそらく、どんな言語を使っていてもまともなプログラマならば最終的にこの本に書いてあるようなところへ行き着くのではないだろうか、と思っている。しかしながら、多くの人が読む本として出版された書籍に書いてある知識と自分の経験則を比較してそこまで間違ってない、と定期的に確認する作業は重要であるし、今後誰かを指導する立場になったときに、「これ読んどいて」と渡せる本があるのは大事な事だと思う。
この本は非常に読みやすいし、文章量も多くない。ものすごく丁寧に読んでも1週間はかからないだろう。私は結局1〜2日で読んでしまった。
そうだよなぁと深く同意する点
ほぼすべてのことに同意できるのだが、その中でも「やっぱこうすべきだよなぁ」と思った点を列挙してみる。
min/max, first/last, begin/endの使い分け
これそうだよなぁ。min/maxは対象の数字を包含した限界値であり、first/lastはlastが包含されている範囲[first, last]を示す。begin/endはendが包含されていない範囲[begin, end)を示す。こういう英語の微妙なニュアンスの違いがコード中に意図として反映されていると読みやすい。ただ、英単語の意味が分かっている必要があるが…。
あんまり関係ない話だけど、私が前やっていたプロジェクトでデータベース中のカラムに「SINE」というものがあり、技術的なデータを扱う業務だったので私はずっと正弦波のsineだと思っていたのだが、これは「サイン」、つまり「Sign」と書きたかったらしい。その列は結局なんだったのかというと、そのデータをチェックした人の名前が入る列だった。つまり、署名(signature)と書きたかったのである。このせいで私は何時間も作業が遅滞してしまった。
個人的な意見だが、英語が書けないなら日本語で書けば良いのである。ローマ字でも良いし、今の処理系は大体UTF-8を理解するから日本語をそのまま書いたって良い。それをば、なんですか?カッコつけて苦手な英語を使ってこうやって人に迷惑をかけるというのは。「SHOMEI」と書くほうが「SINE」とか意味不明なことを書くよりもずっとマシだ。
ネストを減らす
こんな記事を昔書いた→コードのネストを深くするな
関数の途中でReturnする
コードのネストを深くするなにも書いたが、関数の途中でreturnすることは何ら問題がない。昔はこれが良くないとされる理由が何かあって、その名残だと思うのだが、本書では「エラー時のリソース開放処理などが抜けてしまうという理由があったのではないか」と考察していた。
単位を書く
時間やデータサイズを指定するときは、単位を明記するべきだ。本書で推奨されているのは、変数末尾に_ms、_sec、_kb、_mbなどの単位を書いておく方法だ。これは私も昔からやっていた。これを書いておかないと、いちいちAPIマニュアルを参照する羽目になって面倒くさい。ソースコードを読んで判別する場合も多々ある。これはすごく無駄な時間だ。私は単位がある物理量を扱うときは必ず末尾に単位を書くようにしている。
そうかな?と思った点
細かいところで大筋の議論とは離れたツッコミなのだけど、一応。
filter()が曖昧
filterという関数は多くの言語で目にする。employees.filter(dept -> dept == "営業")みたいなコードですね。filterという名称は、続く式が真のものを集めるのか、偽のものを集めるのか(=真のものを除外するのか)が曖昧だという。確かに英語の意味を考えたらそう思うが、しかしfilter関数で後者のような実装になっているものを私は見たことがない。まず間違えないような気がする。英語圏の人だとちょっと感覚が違うのかもしれないけど…。
列を揃える
example_function("yamamoto", "kazuhiko", 35); example_function("shinagawa", "ryou", 29); example_function("akiyama", "kouitchi", 21); example_function("hata", "yoshio", 32);
こういうのを、
example_function("yamamoto", "kazuhiko", 35); example_function("shinagawa", "ryou", 29); example_function("akiyama", "kouitchi", 21); example_function("hata", "yoshio", 32);
みたいな。
確かに読みやすいんだけど、揃えるのが面倒だからよほどの理由が無い限りは私はこういうことをしない。一つ長いものを入れたら全部修正が必要になるしね。タブを使っても良いのだけど、タブはエディタによって表示がバラバラに鳴るから好きじゃない。このあたり、IntelliJとかだと簡単にできるのかな?
読みやすさと関数型プログラミング
私は平凡なプログラマにとっての関数型プログラミングという記事において、関数型プログラミングは「読みやすいコードを書く」という点で重要ということを記した。本書を読んでいても、それ、Scalaならこう書けるよ、みたいなのが色々あったので紹介したい。あんまりやるとScalaハラスメントになるので、まあ参考って感じで。
三項演算子
私が三項演算子を使う一番多いケースが変数に値を簡潔に入れたいときだ。
String name; if (tmp_name.endsWith("さん")) name = tmp_name; else name = tmp_name + "さん";
上記のコードは、明らかに下のように書き直したほうが読みやすいと私は思う。
String name = tmp_name.endsWith("さん") ? tmp_name : tmp_name + "さん";
すると代入が一つしか無いから、「nameってのは必ず『さん』が付いているんだな」という意図が一発で分かる。if文が続くと最後まで読まないと意図がわからないので読みにくい。
しかしながら、三項演算子自体がちょっと読みにくいので、処理を複数のステートメントに分けなければならないような場合では素直にif文を使うしか無い。でもScalaならifは式で値を返すので、もう少しわかり易くかける。
val name: String = if (tmp_name.endsWith("さん")){ /* ケース1 */ /* ... */ } else { /* ケース2 */ /* ... */ }
みたいな感じだ。特に変数のスコープを明示しておきたいなどの理由がある場合は、ブロックを使うこともできる。ブロックも値を返す。
val name: String = { /* ... */ /* ... */ }
こうすることで、「この処理は面倒な初期化処理だからまとめておきたいのだけど、そのために関数を作るのもめんどい。そこまで長いわけではないし、再利用するわけでもない…」みたいな場合にスマートに書ける。
最初の空でない値を取ってくるOR
PythonやJSでは下記のような書き方ができる。
value = a || b || c;
こうすることで空でない値をa, b, cの中から上手いこと選んでくれる。知っていれば便利だが、知らないと非常にわかりにくい書き方だ。知らない人であれば、valueにはtrueかfalseのbooleanが入っていると思うに違いない。これがもっと特殊な記法であれば「何だこれ」と不審に思わせる効果があるのだが、それすら無いので間違えやすい。
ScalaではOptionがあるのでこういう書き方をせずに済みます。対象が二個であれば、
value = a getOrElse b // bの型はOption[T]のT
と書けます。分かりやすいですね。3つならば
value = a.getOrElse(b.getOrElse(c)) // cの型はOption[T]のT
という感じでしょうか。ちょっとださいかな。
Seq(a, b, c).flatten.headOption
だったらまだ許せるだろうか。
ループ処理
本書ではループ処理の書き方について結構な文量を割いて説明しているのだけど、最近の言語ではコレクション操作系の高階関数が充実しているのでループ処理自体を書くことが殆ど無い。Scalaにおけるコレクション操作をする高階関数はたとえば以下のような感じ。
aggregate, combinations(組み合わせ、nCrのアレ), collect, collectFirst, contains, count, diff(差集合), distinct, dropRight, dropWhile, exists, filter, filterNot, find, flatMap, fold, foldLeft, foldRight, forall, foreach, groupBy, indexOf, map, max, min, partition(要素それぞれを2つのタプルに分ける。zipの逆), permutations(順列、nPrのアレ), reduce, reduceOption, reverse, slice, sliding(要素を決まった窓長でスライディングしていく。スライディングウィンドウ), splitAt, take, takeWhile, toMap, toSet, zip, zipWithIndex(要素を要素とインデックスのタプルに変換)
あたりが私がよく使う関数ですね。どうです?これだけ充実してたらforループなんていらなくないですか?
Setset = new HashSet (); List list = new LinkedList (); for( String name : nameList ) if(!set.contains(name)){ set.add(name); list.add(name); }
と書くと、7行書いてようやく重複を削除して順序が保存されたリストがほしいのだな、と理解できますが、これは
names.distinct
と書いたほうが分かりやすいのは明らかですね。
aggregate, fold, reduce, foreachなどもあるので書こうと思えば何でも書けちゃうんですけど、たまにこういったコレクション操作関数を使うよりも単純にforループで手続き的に書いたほうが分かりやすいという場合もあります。
再代入不可能な変数
本書では変数は一度だけ書き込んだほうが分かりやすいと書いていた。どの値が不変なのかが言語仕様レベルで定義されているともっと良い。Scalaではこれを表すためにvalとvarが用意されている。ES6でもlet, constが使用できる。
Power assert
真偽値しか受け取らないようなアサーションメソッドは値がどうなって失敗したのかわかりにくいので、assertEquals(a, b)のような新しいインタフェースを積極的に使おうという話があった。今はPower assertというその名の通りもっと強力なアサーションメソッドが使用できる。発端はGroovyみたいだけど、今は大体の言語で利用できる…はず。
まとめ
本書は「面白い」は本当だった。色々なことを考えた。Scalaでコードを簡潔に書くためのノウハウを満載した本をだれか書いてくれないかな。