3 min read

良いコードを書くために気をつけること

はじめに

何かを学ぶ場合、最初は基礎から入るのが普通だ。つまり、その学ぶ事柄に関する動き構成するいくつかの要素の型を身につける。例えば、私が最初に水泳を習ったのは幼稚園の時だが、幼稚園にいる間の殆どの水泳の時間をキックの練習に使った。プールの縁に座って水を蹴ったり、ビート板を抱えて足の動きだけで前に進んだりするのだ。僕達が必死で足を動かしているあいだ、水泳の先生は生徒の足を持って足の動かし方を矯正していく。

なぜ、このようなカリキュラムになっているかというと、代表的な4つの泳法(背泳ぎ・平泳ぎ・バタフライ・クロール)に共通するのが足で水を蹴る動作だからだ。つまり、キックの習熟の良し悪しが泳法全てに影響してしまうのだ。何かを学ぶ時に基本的な動作に悪い癖がついたり、その重要性を認識していないと後々困ったことになる。

これに関連して、個人的にプログラムの勉強に関してはかなり後悔していることがある。 C++/C を真面目に学ばなかったことではない。コードの書き方(writing style)に無頓着だったのだ。プログラムは書くことより読まれることが多いと言われる 1。つまり、可読性(readability)を意識する必要がある。乱雑・非統一的・冗長なコードは有益なものになりにくい。

プログラミングをゼミで勉強したとき、このことの重要性をまったく認識していなかった。もしかして、誰かに言われたり、そういう本が出ているのを知っていたのかもしれない。多分、「はいはい、なるほどね」ぐらいで適当に流していたのだろう。普通の文章は、他人が読むことを意識して書くことが多いという事実を考えれば想像できそうなものなのだが。

良いコードを書く方法について勉強し始めたばかりなのだが2、いくつか勉強したことがあるのでここにメモしていきたいと思う。具体的には、QuantEconの中の1章のWriting Good CodeKenji Satoさんから頂いた指摘、そしてRに関してはAdvanced RのStyle guideを参考に記事を作成した。守るべき原則を良くないコードの例を示して修正する形で記事をすすめていきたい。

守るべき原則

QuantEconの中のWriting Good Codeでは、良いコードを書くために守るべき教義が挙げられている。それは、

  • マジックナンバーを使うべからず
  • 繰り返すべからず (DRY: Don’t repeat yourself)
  • 関数・クラスを使え
  • グローバル変数を使うべからず

である。この4つを一つずつ説明していこう。

マジックナンバーを使うべからず

ここでいうマジックナンバーとは、具体的な数字のことである3。例えば以下のコードを見てみよう。

 vector <-  sample(1:99, 5)
 for (i in 1:5) {
  print(vector[i])
 }
## [1] 9
## [1] 30
## [1] 82
## [1] 52
## [1] 14

出力にもあるように、このコードは適当な1-99のランダムな整数を入れたベクトル4を用意して、それをだすコードである。マジックナンバーとは、このコードでいうところの for(i in 1:5)5 の部分である。最初にベクトル vector を用意するときにと指定したのだが、具体的に数字を入れることで問題のあるコードになっている。具体的には、

  • この 5 がどのような意味を持つ数字なのか
  • ベクトルの長さが、例えば 100 に変わった時、for の中身も変更しなくてはいけない

という2点の問題がすぐに挙げられる。以下のように書き換えると読みやすくなる。

 vector <-  sample(1:99, 5)

 for (i in seq_along(vector)) {
  print(vector[i])
 }
## [1] 30
## [1] 20
## [1] 81
## [1] 35
## [1] 72

繰り返すべからず (DRY: Don’t repeat yourself)

繰り返すべからず、というのは奇妙に聞こえるかもしれない。プログラムは forwhile といった繰り返し処理が得意だからである。もちろん、ここで言う繰り返すべからず (以下、DRY)とは冗長(repepetive)なコードを避けよということである。これの反意語として、WET (We love typing)5 がある。

例えば、以下のようなコードについてあなたはどのような感想を抱くだろうか?

 sampleVector <-  sample(1:99, 5)
 answerVector1 <- seq_along(sampleVector)
 answerVector2 <- seq_along(sampleVector)
 alpha <- 2
 beta <- 3

 for (i in seq_along(sampleVector)) {
  answerVector1[i] <- sampleVector[i] * alpha
 }
 for (i in seq_along(sampleVector)) {
  answerVector2[i] <- sampleVector[i] * beta
 }
 
 print(answerVector1)
## [1]  70 150  90  32 104
 print(answerVector2)
## [1] 105 225 135  48 156

わざわざからのベクトル answerVector1, answerVector2 を定義する必要がなさそうだという他に、 for ループを2回も書いているのが気になる。パラメタが変わっただけの同じようなコードを書くのは無駄な印象を受ける。

例えば、

 parameter <- c(2, 3)

 for (p in parameter) {
  A <-  sample(1:99, 5)
  print(A * p)
 }
## [1]  96  42 166  64 182
## [1] 219  66 201 267 249

とすれば、短くてわかりやすいコードになる。

関数・クラスを使え

関数やクラスを使わなくても6先のコードの無駄な記述を減らすことはできる。しかし、一般的に関数やクラスを定義すると同じ機能を何度も呼び出すことができるので便利だし、記述も少なくてすむ。先のコードを例にすると、

 multipleRandomArrayBy  <- function(parameters){
  for (p in parameters) {
    sampleVector <- sample(1:99, 5)
    print(sampleVector * p)
  }
 }

としておけば、ランダムな要素を持つベクトルにパラメタをかけるという作業を自動でやってくれる。実際、

 parameters <- 1:5
 multipleRandomArrayBy (parameters)
## [1] 25 22 38 87 64
## [1]  34 138  60 192  66
## [1] 249 138  18  36 189
## [1]  44  64 168 216 132
## [1] 440 250 350 340 335

となる7

グローバル変数を使うべからず

関数やクラスの外で定義して、自由に呼び出すことのできるグローバル変数は便利だが、危険な代物である。

なぜグローバル変数が危険かというと、

  • プログラムのどの部分にも影響を及ぼし(→ 変数が影響を与える範囲を制限できない)
  • どんな関数においてでも変更されやすい(→ 変数が変更されうる場所を特定できない)

からである。つまり、グローバル変数がコードのどの範囲に影響をもたらすかを特定することが困難になる危険性があるのだ。

具体例で見てみよう、先の関数と同じようなコード

 vector <- c(1:5)

 multipleArrayBy <- function(parameters){
  for (p in parameters) {
    print(vector * p)
  }
 }

 parameters <- 1:5
 multipleArrayBy(parameters)
## [1] 1 2 3 4 5
## [1]  2  4  6  8 10
## [1]  3  6  9 12 15
## [1]  4  8 12 16 20
## [1]  5 10 15 20 25

をみてみよう。このコードでベクトル vector は関数の外で定義されている。もしかして、問題ないと感じるかもしれない。しかし、次のようなコードを見た時はどうだろうか?

 vector <- c(5:1)
 parameters <- c(1:5)
 multipleArrayBy(parameters)
## [1] 5 4 3 2 1
## [1] 10  8  6  4  2
## [1] 15 12  9  6  3
## [1] 20 16 12  8  4
## [1] 25 20 15 10  5

これは、先の結果と異なっている。その原因は、ベクトル vector を書き換えたからである。さて、問題は上のコードブロックを見ただけで結果が変わった原因を容易に特定できるかどうかという点である。たとえば、 parameters の内容を変更すれば、関数の返り値も変わることは容易に予想ができる。それは、 parameters が関数の引数になっているからである。しかし、今回の問題では、関数の定義部分に戻らなければベクトル vector がグローバル変数を用いていて、それが変更されたことが原因であると特定することは難しい。 multipleArrayBy (parameters)  という記述部分ではベクトル vector に関数記述がないからである。

今回の場合は、vector が書き換わっているのが2行上のコードだったから良い。もしこれが、100行上だったら? もしかして、関数の中だったら? 多分、エラーの原因を特定するのは困難になるだろう。しかも、丁寧にベクトルには vector という誰でも思いつきそうな名前が付けらている。もしかして、コードの別の部分で別の使用用途のために新たに定義されて使われてしまうかもしれない。

名前について

これは、教義の中に入っていないのだが、変数や関数の名前はできるだけ意味のあるものの方がよい。たとえば、

 f <- function(p){
  for (x in p) {
    print(c(1:5) * x)
  }
 }

 p <- c(1:5)
 f(p)
## [1] 1 2 3 4 5
## [1]  2  4  6  8 10
## [1]  3  6  9 12 15
## [1]  4  8 12 16 20
## [1]  5 10 15 20 25

というなんの情報もない関数名や変数よりも、

 multipleArrayBy <- function(parameters){
  for (p in parameters) {
    print(c(1:5) * p)
  }
 }

 parameters <- c(1:5)
 multipleArrayBy(parameters)
## [1] 1 2 3 4 5
## [1]  2  4  6  8 10
## [1]  3  6  9 12 15
## [1]  4  8 12 16 20
## [1]  5 10 15 20 25

という名前の付け方の方がわかりやすいだろう。

まとめ

自分もプログラムに関しては勉強し始めて日が浅いので、学ぶことが多い。なぜプログラミングを書く必要があるのか? という問に対する答えは沢山ある。研究で使うためという人もいるし、再現性を確保するためと考えている人もどうやらいるようだ。

書く理由に関わらず確実に1つ言えることは、書かれたプログラムは他人が読み返すことが多いということである。論文を出版したり、教科書を書いたりした著者がコードを公開することは一般的である。コードを公開しなくても、査読のプロセスで査読者から求められたりすることもあれば8、共著者や指導教授に要求されることもあるだろう。たとえこれらに当てはまらなかってたとしても、一年後の自分は他人である。プログラムを書いた時のことなんてすっかり忘れてしまっているだろう。

個人的には、良いコーディングを身につけて有益な物を生み出したいと思っている。このポストでは触れなかったが、コードにコメントを付けて説明を加えることも大切である9。コメントの内容も他人が読むことを前提として記述することが大切である。くれぐれも、個人的な感想などを書いてはいけない10


  1. 出典不明

  2. 記事を書く一週間前

  3. マジックナンバーという用語は他の意味で使われることもある。例えば、人間は3つ列挙されると理解やされやすいという意味でマジックナンバー3と呼ばれることがあり、スティーブ・ジョブズは自身のプレゼンで3という数字にこだわったと言われている。

  4. Rでは数字を一列に並べて格納しているデーター型のことをベクトルと呼ぶ(参考)。そして、複数次元で格納されているデーター型を配列と呼び、特に2次元で格納されているものを行列と呼ぶ(参考)

  5. We Love Typing でWLTの気がするのだが、DRYと対応させたのだろうか。

  6. Rでもクラスを定義して使用することができるようだが、筆者は未体験である。

  7. ここまで来てお気付きの読者もいるかもしれないが、このプログラム、全くの無意味である。あくまで、説明のために使った。

  8. あくまで伝聞である。筆者の体験ではない。

  9. Rであれば# の後はコメントとなってプログラムにコメントをかける。

  10. これは個人的な体験にもとづいているわけではなく、一般論である。