背景
プログラム開発プロセスを見直したいと思っていた。
変数の名前やコメントの入れ方など、色々勉強&工夫はしているが、根本的な方法論が不足しているのは明らかだった。
データ分析系の仕事では、製品開発のプログラミングスタイルは個人の生産性が低く、明らかに合わない。
だが、検証やドキュメンテーションの不備は、たいてい納期直前に地獄を見ることになる。
この板挟みをうまく解消する方法はないだろうか?
そこで、最近、以下の記事を読んだ。
忙しい研究者のためのテストコードとドキュメントの書き方
これは非常にいい。Rでも実践したいと思った。
RStudioの機能を使う
まずPythonのdocstring的なものを用意しないといけない。
幸いなことに、RStudioでは、関数の中でCtrl+Alt+Shift+Rでdocstring的なもの (Rで別の名前がついてるのだろうか?) を作れる。
関数の引数を書いて、関数の中で上記コマンドを押せば以下のようになる。
#' Title
#'
#' @param x
#' @param y
#'
#' @return
#' @export
#'
#' @examples
comp_eigen <- function(x, y) {
}
ここから開発をはじめる。
以下、作業は全てRStudio上で行う前提。
docstringを書く (入出力編)
関数のやること、引数、戻り値を書く。
#' 2つの行列の固有値の内積を求める
#'
#' @param x 行列1 (matrix)
#' @param y 行列2 (matrix)
#'
#' @return 内積の値 (num)
#' @export
#'
#' @examples
comp_eigen <- function(x, y) {
}
これで入出力を定義できた。
普段はこれでいいが、もし将来的にパッケージ化する場合、英語じゃないとだめ (というか2バイト文字がだめ) っぽいので、気をつける。
docstringを書く (テストコード編)
次に、@examplesの下にこの関数を使った処理を書く。
ここで書く処理は、サンプルであり、テストコードになる。
#' 2つの行列の固有値の内積を求める
#'
#' @param x 行列1 (matrix)
#' @param y 行列2 (matrix)
#'
#' @return 内積の値 (num)
#' @export
#'
#' @examples
#' set.seed(0)
#' x <- matrix(rnorm(9), 3, 3)
#' y <- matrix(rnorm(9), 3, 3)
#' comp_eigen(x, y)
comp_eigen <- function(x, y) {
}
本当はcomp_eigen()の出力値に関する想定も入れた方が良いのだが、今回はなしで(動作確認までしかテストしないことになる)。
答えがわかったら、コメントに追記しておくと、将来的なアップデートをした際に結果を検証できるので良いだろう。
ちなみになぜこれがテストコードになるかだが、@examplesより下の部分にカーソルをあわせてCtrl + Enterを押すとその行を実行できる。
とても便利な機能だが、知ったのはつい最近だ(むしろこれを知ってればもっと前から@examplesにテストコード書いてたよ……)。
関数の中身を書き込む
中身を書き込んでいく。
#' 2つの行列の固有値の内積を求める
#'
#' @param x 行列1 (matrix)
#' @param y 行列2 (matrix)
#'
#' @return 内積の値 (num)
#' @export
#'
#' @examples
#' set.seed(0)
#' x <- matrix(rnorm(9), 3, 3)
#' y <- matrix(rnorm(9), 3, 3)
#' comp_eigen(x, y)
comp_eigen <- function(x, y) {
x_value <- eigen(x)
y_value <- eigen(y)
x_value*y_value
}
中身を一通り書けたら、次のステップに移る。
テストする
関数を読み込んだ後、@examplesの行をCtrl+Enterで実行する。
結果が正しく出力されるか、どんなエラーが出るかを確認する。
> comp_eigen(x, y)
Error in x_value * y_value : non-numeric argument to binary operator
エラーが出てきた。最後の行がおかしいらしい。
デバッグする
browser()
を使ったりして、途中の状態を確認しながら直していく。
よくみると、そもそもx_valueの値が思ったのと違うようだ。固有値ではなく、リスト構造になっている。
Browse[1]> x_value
eigen() decomposition
$values
[1] 1.0382130+1.21338i 1.0382130-1.21338i -0.4045975+0.00000i
$vectors
[,1] [,2] [,3]
[1,] 0.7280958+0.0000000i 0.7280958+0.0000000i 0.1940062+0i
[2,] 0.0169427+0.2529073i 0.0169427-0.2529073i 0.3995693+0i
[3,] 0.1994380-0.6048569i 0.1994380+0.6048569i 0.8959386+0i
ここから欲しいのは固有値($values
) だけだ。
ここを修正すると、同様にy_valueも修正することになる。
その後、戻り値が3つ返ってきて「あれおかしいぞ?」となり、内積を求める計算も、*
でもないということに気づく。
最終的に、以下のようになる。
#' 2つの行列の固有値の内積を求める
#'
#' @param x 行列1 (matrix)
#' @param y 行列2 (matrix)
#'
#' @return 内積の値 (num)
#' @export
#'
#' @examples
#' set.seed(0)
#' x <- matrix(rnorm(9), 3, 3)
#' y <- matrix(rnorm(9), 3, 3)
#' comp_eigen(x, y)
comp_eigen <- function(x, y) {
x_value <- eigen(x)$values
y_value <- eigen(y)$values
x_value %*% y_value
}
一通り動くようになる
テストコードを実行すると、以下の出力になる。
> comp_eigen(x, y)
[,1]
[1,] 1.688619+3.510959i
というわけで、ひとまず完成した。
リファクタする
今回はあまり必要ないと思うが、ある程度以上の複雑なプログラムを書いていると、リファクタが必要になるケースがほとんどだ。
リファクタ自体は、もともとやっている(はずな)ことなので、このプロセスにおいても当然必要だ。
完成
以上のプロセスで、めでたく「関数、ヘルプ、テストコード」の3つ組が完成した。
これなら半年後の自分もこのコードを見て絶望することはないだろう。
余談
実際には、テスト~デバッグは何周もグルグルすることになる(上記の例では、x_value,y_valueの修正と、内積の計算の修正で2周)。
何周するかは、プログラムの複雑度と、事前の設計、スキルなど様々な要因に依存する。
上記のやり方を実践した結果、今までに気になっていた点はかなり解消できた。あと完成した時に気持ちが良い。
一方で、以下のスライドのつまづきに見事にハマり始めた。レガシーコードの谷は非常に深い。
理想のプログラミングスタイルの実現はまだまだ遠いらしい。
[TDDを実践してわかったTDDつまづくあるあると自分なりの乗り越え方まとめ] (https://www.slideshare.net/keiswd/tddtdd)