よりそいプログラミングのすすめ
ちょっと前の話なんですけれど,あたしが知ってる生身の人の中で,もっとも優秀なプログラマさんのコーディングを見せてもらうことがありました。知らないライブラリの使い方をたずねたのがきっかけなんですけれど,「ちょっと作ってみるから見てて」ってな話になったのでした。これがすごかった。
「よりそいプログラミング」というのは,ここで作った言葉で,単純に片方が寄り添う形で行うプログラミング方法です。片方がひたすらプログラミングして,それを傍から見るというモノ。ここで,似たような言葉のペアプログラミングは,もちろん製造工程の話だけれど,「よりそいプログラミング」は開発工程とは異なる,教育目的のプログラミングです。だから,生産性云々とかは,とりあえずさておいてください。
さて,そのプログラマさんのプログラミングを見ていたところ,とにかくペースがものすごい。傍から見ていると,言葉を出すのと同じ感覚で,考えたことをソースコードに反映している印象があります。あと,これはとても参考になったんですけれど,ソースコードの編集テクニックのような,「完成品」になるまでの途中経過を見ることができました。おそらく,これは某氏の手癖になっているんでしょうけれど,非常に効率のいい編集テクニックを直接見せてもらって感心しきり。
入門書なんかでは,できかけのソースを見せるってことがありません。んなもんで,例えば,C/C++ の初心者さんなんかは,関数を編集するときに頭の静的変数の宣言から,順番に書くもんだと思っている向きも少なくないと思います。けれど,実際の製作過程はそうじゃなかったりする。ベテランさんが作るソースがどのようにできるのかを見せるのは,教育上とてもいいんじゃないかと思ったりします。
ともあれ,言葉であれこれ書いてもアレなので,擬似よりそいプログラミングとして,簡単な例を示します。あたしの例で恐縮なんですが,あたしの隣でモニタを見ている気分で読んでみてください。お題は,「引数で受け取った文字の中にカンマがいくつあるか数える関数を作る」ってなことにしときましょう。言語は C/C++ です。
まず,関数の名前,引数,戻り値を決めて,呼び元と呼び先(目的の関数)を作ります。ここでは,int count_comma(const char* str) にすることにしました。ここで,呼び元は簡単な単体テストケースとして使うことができます。
int count_comma(const char* str) { int ret; return ret; } int main(int argc, char* argv[]) { int ret; const char* str = "hello, world, world, world,,"; ret = count_comma(str); printf("%d\n", ret); return 0; }
ここまではぱぱっと書けますよね。ヘッダファイルはまだ書きません。必要になる度に書くのは面倒だからです。
で,中身を考える。理屈としては,引数(入力文字列)を走査して,カンマが出てきたら返値をインクリメントしていけばよさそうです。返値はもう用意してあるので(ret),走査する箇所を作りましょう。for-文なりwhile-文でクルクル回せばよさそうです。こんな感じでしょうか。
int count_comma(const char* str) { int ret; for (ptr = str; *ptr != '\0'; ptr++) { } return ret; } int main(int argc, char* argv[]) { int ret; const char* str = "hello, world, world, world,,"; ret = count_comma(str); printf("%d\n", ret); return 0; }
あたしの場合,その関数で使うローカル変数の宣言は,その場で書きません。いきなり ptr とかって変数が出てきたけど,後で宣言すればいいんです。とにかく,ptr なる変数は str の頭からお尻まで1文字ずつ走査していくことになります。
で,やることは,カンマの数を数えるのでした。ここで,いきなりカンマを直書きしてもいいんですけれど,あたしゃ #define か,static const で宣言することにしています。つことで,カンマを宣言しておく。
static const char COMMA = ','; int count_comma(const char* str) { int ret; for (ptr = str; *ptr != '\0'; ptr++) { } return ret; } int main(int argc, char* argv[]) { int ret; const char* str = "hello, world, world, world,,"; ret = count_comma(str); printf("%d\n", ret); return 0; }
で,for-文に戻る。カンマが見つかったら,ret をインクリメントするんでしたね。こう書きます。
static const char COMMA = ','; int count_comma(const char* str) { int ret; for (ptr = str; *ptr != '\0'; ptr++) { if (*ptr == COMMA) { ret++; } } return ret; } int main(int argc, char* argv[]) { int ret; const char* str = "hello, world, world, world,,"; ret = count_comma(str); printf("%d\n", ret); return 0; }
おっと,ret を初期化してませんでしたね。初期化します。
static const char COMMA = ','; int count_comma(const char* str) { int ret; ret = 0; for (ptr = str; *ptr != '\0'; ptr++) { if (*ptr == COMMA) { ret++; } } return ret; } int main(int argc, char* argv[]) { int ret; const char* str = "hello, world, world, world,,"; ret = count_comma(str); printf("%d\n", ret); return 0; }
必要そうな機能はそろったつもりなので,一度見直してコンパイルしてみます。今回は小さい関数なので,足りないところがすぐに分かるんですけれど,大きい関数を作る時は,大体できたと思ったらまずコンパイルして,エラーメッセージを出してみます。
$ gcc test.cpp test.cpp: In function `int count_comma(const char*)': test.cpp:8: error: `ptr' undeclared (first use this function) test.cpp:8: error: (Each undeclared identifier is reported only once for each function it appears in.) test.cpp: In function `int main(int, char**)': test.cpp:24: error: `printf' undeclared (first use this function)
Cygwin gcc なので英語のエラーメッセージなんですけれど,こんなこと言ってます。
test.cpp: `int count_comma(const char*)' 関数で:
test.cpp:8: エラー: `ptr' が宣言されていません (この関数で初めて出てきてます)
test.cpp:8: エラー: (宣言されていない識別子はそれが現れた関数ごとに1回だけ報告されます)
test.cpp: `int main(int, char**)' 関数で:
test.cpp:24: エラー: `printf' が宣言されていません (この関数で初めて出てきてます)
ptr と printf を宣言していませんでした。いけねい,いけねい(←わざとらしい)。つことで,宣言しましょう。ptr は自前のローカル変数なので,count_comma() 関数の冒頭で宣言すればいいですよね。一方,printf() は標準関数の識別子なので,stdio.h を include する必要があります。これでいいでしょうか。
#include <stdio.h> static const char COMMA = ','; int count_comma(const char* str) { int ret; const char* ptr; ret = 0; for (ptr = str; *ptr != '\0'; ptr++) { if (*ptr == COMMA) { ret++; } } return ret; } int main(int argc, char* argv[]) { int ret; const char* str = "hello, world, world, world,,"; ret = count_comma(str); printf("%d\n", ret); return 0; }
つことで,もう一度コンパイルしてみる。
$ gcc test.cpp $
できた。いや,コンパイルが通っただけですね。ここからテストに移りましょう。とりあえず実行。
$ ./a 5
呼び元(main())の str にあるカンマの数は5つあるので,なんかうまく動いているみたいです。とりあえず,異常系の検査もしてみます。こゆのはやりだすとキリがないんですけど,ここでは次のような検査をしてみます。括弧内は想定される結果です。
- str が NULL の場合(0を返す)
- カンマがない場合(0を返す)
- ヌル文字の場合(0を返す)
str の値を変えて試してみます。すると,ここでは str が NULL の場合にこんなエラーが出て死んでしまいました。
$ ./a 20421 [main] a 1660 _cygtls::handle_exceptions: Error while dumping state (probably corrupted stack) Segmentation fault (core dumped)
コアを吐いて死んでます。診断はセグメンテーションフォルトとのこと。これは,参照できないアドレスを参照した時に出るエラーですよね。Windows では,0x00000005 の返値と一緒に access violation とか出るアレです。ここでは,NULL(通常は0番地)を参照したので怒られたのでした。
エラーは count_comma() の中にあると分かっているので,中を見てみると,for-文の *ptr != '\0' で NULL 番地の値を参照しています。ここで落ちたんでしょう。つことで,NULL の場合は何もしないで 0 を返すように修正します。
#include <stdio.h> static const char COMMA = ','; int count_comma(const char* str) { int ret; const char* ptr; ret = 0; if (str != NULL) { for (ptr = str; *ptr != '\0'; ptr++) { if (*ptr == COMMA) { ret++; } } } return ret; } int main(int argc, char* argv[]) { int ret; const char* str = "hello, world, world, world,,"; ret = count_comma(str); printf("%d\n", ret); return 0; }
これで実行したら,str が NULL の場合にも対応できました。単体テストも全部通ったので(ほんとはもっとやるんだけど),これで出来上がりです。
と,こんな感じで,他人がソースコード作る様子を見るのが,よりそいプログラミングです。どうでしょう。「すげーテクニックだ!」と思った方もいるかもしれませんし,「なんか危なっかしいな」と思った方もいると思います。「自分ならこうするのに」と思う方もいるかもしれません。いずれにせよ,他人のプログラミングの「過程」を見るだけで,なんらかの刺激があったんじゃないでしょうか。
あたしの場合ですけれど,こういうことを経験できる機会はあまりなかったりします。大体,プログラミングってひとりでしますしね。けど,この効果は特に初心者さんに効果的だと思っていて,具体的な製作手順を見せてもらうと,作業の手順を具体的につかむことができるんじゃないかと思います。よりそいプログラミング,おすすめ。
ちなみに,上の手順は,あらかじめ用意したものではなくて,本当にプログラミングしながら書きました。だから NULL のケースを見逃したのも天然。危なっかしいですね。あたしの場合は,危なっかしいですけれど,ベテランさんの側に寄り添ってコーディングを見せてもらうと,「正しい」プログラミングのリズムとかペースなんかもつかむことができたりします。多分,異次元だと感じるはず。お近くのベテランさんを引っ張ってきて,是非とも生で体験してもらいたいところ。