UECジャーナル

作法その十、思いついた事は、すぐコンピュータに相談せよ

昔、それはそれは世にも奇妙な世界を描いた劇があった。劇中、客の注文を聞き間違えてしまったラーメン屋の店員、原稿を読み間違えてしまった放送局員。彼らは直後に皆、国家公認と思われる狙撃部隊によって射殺されてしまう。なんとその世界では、どんな些細なことであれ、間違いを犯すようないい加減な人間には、法的に生存権が認められないのだ。

もしそんな世界に生まれていたら、プログラマなど真っ先に絶滅するに違いない。不具合が1つでも残ったプログラムを一度たりとも公開しようものなら最後、途端にこの世から消されてしまうのだを実施する。

幸いにしてこの現実世界では、不具合が残っているだけで命を奪われることはまずない。しかし不具合が残るプログラムの公開は、多かれ少なかれ本人にも不幸をもたらすことは間違いない。今回の作法は、そんな不具合を残さないプログラミングを行うためにも役立つ作法である。

思いもよらぬ事は一人で考えるほど生じる

だが、公開する時点で不具合のまったくないプログラムなど作れるものなのだろうか。些細なものを含めれば公開後にも必ず不具合の残るプログラムしか作れなかった筆者からすると、それは至難の業に思える。

たしかに、徹底的なデバッグをして、想定外の事態を徹底的に予防すれば、理屈の上ではそういった事態を事前に克服できることだろう。しかし、問題を1つ残らず事前に想定し尽くすとなると、さすがに不可能ではないだろうか。「さすがにここまで見直しておけば完璧だろう」と思うまで見直しても、思いもよらない問題に、必ずといって良いほど遭遇してきた。

ではそんな、「思いもよらないこと」を防ぐためにはどうすればいいのだろうか。経験から言えば、たくさんの人に相談することが効果的であると思う。自分自身にとっては思いもよらないことが、他人にとってはそうではないということはよくあるものだ。

また、相談は、早いうちにすることも重要である。時間が経ては立つほど、問題は増えて複雑化するし、初期に起こった問題を見失ってしまう。そうならないためにも、問題を認識した時点で1つ1つ、可能な限り速やかに相談するのが理想だろう。

コンピュータに相談するということ

相談相手は、必ずしも人でなくてもよい。物に結果を教えてもらう、つまり実験するということだ。実際世の中は、やってみなければわからないことで溢れている。

そして実験もやはり、早いうちにやってみることが重要である。もたもたしていては、実験すべきことがどんどん複雑になっていくし、あるいは何を実験すべきなのかをどんどん忘れていってしまう。

ゆえにプログラミングの世界でも、想定外の動作を防ぐため、速やかかつ頻繁なコンピュータへの相談、すなわち速やかかつ頻繁なプログラミングとデバッグが大切なのである。

実践に則した制作例

それでは、実際にユニケージエンジニアはどのような要領でコーディングとデバッグを行っているのか、実践に則した例を紹介する。

今、ある小売店から、次のような集計をするプログラムの制作を依頼されたとする。

1:日付、2:レシート番号、3:購買品合計額、4:購買品原価合計、という4つの列から
構成された表(リスト0)がある。この表を集計し、曜日毎に次のデータを算出せよ。

・客1人あたりの平均購買額(客単価)
・前述した曜日間での比率
・1日あたりの平均売上原価
・1日の平均原価率

リスト0. 計算対象の元データ(先頭部分を抜粋)

20140101 1190482893 200 198
20140101 1190482894 300 40
20140101 1190482895 600 470
20140101 1190482896 1200 21
20140101 1190482897 1000 81
20140101 1190482898 3000 134
20140101 1190482899 400 225
20140101 1190482900 900 217
20140101 1190482901 1200 202
20140101 1190482902 2600 106
20140101 1190482903 1300 510
...

最終的にはもちろん前述した出力を得るプログラムにしなければならないが、最初からそこまでのプログラムを作りはしない。以降で説明するように、目的のデータを得るまでを段階に分け、最初に何を求め、次に何を求め、ということを1つ1つ順番に考えていくのだ。

一、日単位に集計するプログラム

今回の要求の場合、最終的に欲しいのは曜日ごとの集計であるが、まずは日単位に集計する必要があると考えた。リスト1を見てもらいたい。日ごとに客数の合計を求めるとともに各日各列の合計を求め(6行目)、その値をもとに客1人あたりの平均購買額(客単価)、総売上、総売上原価を求めるプログラムを書くことにした。ここまで書いて、実際のデータを流し、デバッグし終えたものがこのリスト1である。デバッグ時に確認する主なことは、列の構成が前述のとおりになっていて、それらしい値になっていることである。

リスト1. 日単位の集計まで行うプログラム

#!/bin/sh

cat DATA                                        |
# 1:日付 2:レシート番号
# 3:購買合計額 4:購買品原価計
sm2 +count 1 1 3 3                              |
# 1:日付 2:当日客数 3:当日総売上
# 4:当日総売上原価
awk '{print $1,$3/$2,$4}'                       |
# 1:日付 2:当日1人当り平均売上(客単価)
# 3:当日1人当り平均売上原価
marume 2.2 3.2

二、曜日単位に集計するプログラム

日単位の集計データがあれば、曜日単位のデータを求めるのは簡単にできる。リスト2を見てもらいたい。日付を7日周期で仕分け(9行目)、同じ曜日同士で集計(13行目)している。これで、曜日あたりの客1人あたりの平均購買額、1日の平均売上、平均売上原価が求められる。

リスト2. 日単位の集計まで行うプログラム

#!/bin/sh

cat DATA                                        |
# 1:日付 2:レシート番号
# 3:購買合計額 4:購買品原価計
sm2 +count 1 1 3 4          			|
# 1:日付 2:当日客数 3:当日総売上
# 4:当日総売上原価
awk '{print NR%7,$2,$3,$4}' 			| # 追加した行
sort                        			| # 追加した行
# 1:曜日番号 2:当日客数 3:当日総売上
# 4:当日総売上原価
sm2 +count 1 1 2 4          			| # 追加した行
# 1:曜日 2:各曜日総客数 3:各曜日総売上
# 4:各曜日総売上原価
awk '{print $1,$3/$2,$4}'   			|
# 1:曜日 2:各曜日1人当り平均売上(客単価)
# 3:各曜日1人当り平均売上原価
marume 2.2 3.2

リスト2はリスト1を改修し、デバッグをしたものだ。デバッグ時に確認すべき項目はリスト1の時と同様、列構成やそれらしい値か出ていることである。今度は曜日単位であるから、正しく計算されているなら全レコードは7行になっているはずである。

三、曜日間の客単価比まで求めるプログラム

各曜日の平均購買額(客単価)が算出されているのであれば、各曜日での比率を求めることは容易である。リスト2の18行目の後に、レコード間の比率を求めるTukubaiコマンドratioを追加すればよい。

書き直した後、デバッグを終えたものがリスト3である。デバッグの要領は、これまでと同様だ。

リスト3. 曜日間での客単価比計算機能を追加したもの

#!/bin/sh

cat DATA                                        |
# 1:日付 2:レシート番号
# 3:購買合計額 4:購買品原価計
sm2 +count 1 1 3 4                              |
# 1:日付 2:当日客数 3:当日総売上
# 4:当日総売上原価
awk '{print NR%7,$2,$3,$4}'                     |
sort                                            |
# 1:曜日番号 2:当日客数 3:当日総売上
# 4:当日総売上原価
sm2 +count 1 1 2 4                              |
# 1:曜日 2:各曜日総客数 3:各曜日総売上
# 4:各曜日総売上原価
awk '{print $1,$3/$2,$4}'                       |
# 1:曜日 2:各曜日1人当り平均売上(客単価)
# 3:各曜日1人当り平均売上原価
ratio key=2                                     | # 追加した行
# 1:曜日 2:各曜日1人当り平均売上(客単価)
# 3:曜日間客単価比 4:各曜日1人当り平均売上原価
marume 2.2 4.2i                                   # 変更した行

四、定休日を考慮するように修正

リスト3は、元々提供されていたデータ(リスト0)を与える限りにおいては正しいデータを出力することができていた。デバッグも行ったので間違いない。

ところが、依頼主の小売店から将来的に定休日を設ける可能性があるという話を知らされた。そうなるとこのプログラムは正しく動かない。たとえば、日曜日が定休日だとすると、元のデータには日曜日のレコードがまったく現れないことになる。この場合、日曜日についての値はデータ上、「不明」扱いになってしまうのだ。

そこで、プログラムに修正を施すことにした。具体的には、元データに対し、休業日であっても便宜的に売上と原価が0であるレコードを追加して曜日集計から漏れないようにした。この修正を施した後、再度デバッグまで済ませたものがリスト4である。デバッグは、元データから特定の曜日のレコードを抜いたもので試してみればよい。きちんと7レコード出てきていれば恐らく正しく修正できているであろう。

リスト4. 定休日があっても正しく動くよう修正したもの

#!/bin/sh

tmp=/tmp/tmp_$$                                                                        # 追加した行
seq 1 31 | awk '{printf("201401%02d\n",$1)}' > $tmp-days # 1:日付(2014/1/1~2014/1/31)  # 追加した行

loopj num=1 $tmp-days DATA                      | # 追加した行
# 1:日付 2:レシート番号
# 3:購買合計額 4:購買品原価計
sm2 +count 1 1 3 4                              |
# 1:日付 2:当日客数 3:当日総売上
# 4:当日総売上原価
awk '{print NR%7,$2,$3,$4}'                     |
sort                                            |
# 1:曜日番号 2:当日客数 3:当日総売上
# 4:当日総売上原価
sm2 +count 1 1 2 4                              |
# 1:曜日 2:各曜日総客数 3:各曜日総売上
# 4:各曜日総売上原価
awk '{print $1,$3/$2,$4}'                       |
# 1:曜日 2:各曜日1人当り平均売上(客単価)
# 3:各曜日1人当り平均売上原価
ratio key=2                                     |
# 1:曜日 2:各曜日1人当り平均売上(客単価)
# 3:曜日間客単価比 4:各曜日1人当り平均売上原価
marume 2.2 4.2

五、目的のプログラム完成

目的のデータを得るまであと一息だ。ここまでのプログラムで、各曜日1日あたりの平均売上と平均売上原価がわかっているのだから、平均原価率を求めるのもまた容易である。リスト4の19行目のAWKを少し直し、原価率の列を増やせばよい。そのように書き直した後、デバッグを終えたものがリスト5である。

リスト5. 各曜日1日当り原価率の列を追加したもの

#!/bin/sh

tmp=/tmp/tmp_$$
seq 1 31 | awk '{printf("201401%02d\n",$1)}' > $tmp-days # 1:日付(2014/1/1~2014/1/31)

loopj num=1 $tmp-days DATA                      |
# 1:日付 2:レシート番号
# 3:購買合計額 4:購買品原価計
sm2 +count 1 1 3 4                              |
# 1:日付 2:当日客数 3:当日総売上
# 4:当日総売上原価
awk '{print NR%7,$2,$3,$4}'                     |
sort                                            |
# 1:曜日番号 2:当日客数 3:当日総売上
# 4:当日総売上原価
sm2 +count 1 1 2 4                              |
# 1:曜日 2:各曜日総客数 3:各曜日総売上
# 4:各曜日総売上原価
awk '{print $1,$3/$2,$4,$3/$4}'                 | # 変更した行
# 1:曜日 2:各曜日1人当り平均売上(客単価)
# 3:各曜日1人当り平均売上原価
# 4:各曜日1日当り原価率
ratio key=2                                     |
# 1:曜日 2:各曜日1人当り平均売上(客単価)
# 3:曜日間客単価比 4:各曜日1人当り平均売上原価
# 5:各曜日1日当り原価率
marume 2.2 4.2 5.2                                # 変更した行

六、集計範囲指定の追加要望への対応

プログラムは完成したが、その後、集計されたデータを見た小売店の担当者から「7日前までのデータを集計」などのように、集計範囲を指定したいという要望が寄せられた。だが、この要望への対応も、ここまで制作してきたやり方をそのまま行うだけである。

つまり、リスト5に対して、元データの集計対象を絞り込むためのコードを書き足すのだ。そのように書き足して、デバッグを済ませたものがリスト6である。

リスト6. 集計する日数を指定できるようにしたもの

#!/bin/sh

tmp=/tmp/tmp_$$
[ -z $1 ] && exit 0                       # 追加した行
for v in $(seq $1); do date +%Y%m%d -d "$v day ago" ; done | sort > $tmp-sitei  # 追加した行
seq 1 31 | awk '{printf("201401%02d\n",$1)}' > $tmp-days # 1:日付(2014/1/1~2014/1/31)
loopj num=1 $tmp-days DATA                      |
join0 key=1 $tmp-sitei                          | # 追加した行
# 1:日付 2:レシート番号
# 3:購買合計額 4:購買品原価計
sm2 +count 1 1 3 4                              |
# 1:日付 2:当日客数 3:当日総売上
# 4:当日総売上原価
awk '{print NR%7,$2,$3,$4}'                     |
sort                                            |
# 1:曜日番号 2:当日客数 3:当日総売上
# 4:当日総売上原価
sm2 +count 1 1 2 4                              |
# 1:曜日 2:各曜日総客数 3:各曜日総売上
# 4:各曜日総売上原価
awk '{print $1,$3/$2,$4,$3/$4}'                 |
# 1:曜日 2:各曜日1人当り平均売上(客単価)
# 3:各曜日1人当り平均売上原価
# 4:各曜日1日当り原価率
ratio key=2                                     |
# 1:曜日 2:各曜日1人当り平均売上(客単価)
# 3:曜日間客単価比 4:各曜日1人当り平均売上原価
# 5:各曜日1日当り原価率
marume 2.2 4.2 5.2

1行書くたびデバッグする

前述の例ではプログラムリストが6つできあがった。そしてデバッグもそのたびにしてきた。意味のあるデータが出来上がるその都度その都度、少し極端な言い方をすればシェルスクリプトのパイプを繋ぐコマンドを1行書き足すたびにデバッグしてきた。

もし、要求された仕様のプログラムを一通り書いてからデバッグをするという工程で制作していたらどうなっていたか想像してみてもらいたい。デバッグのための検査項目はどうやって洗い出すだろうか。冒頭で述べたように、想定外の動作を防ぐためにはここで漏れなく検査項目を列挙しなければならない。果たして、リスト5(あるいは6)を眺め、何を確認すべきかについて必要十分な項目を洗い出すことができるだろうか。結局これが難しい作業なのである。

1行書くたびデバッグをするというのは、デバッグの回数が増えて一見生産効率が悪いように思える。しかしこうして比較すれば、決してそうは言えないことが理解できるだろう。また頻繁に行えば、1回当たりのデバッグにかける時間は当然少ない。それに、デバッグしながらのコーディングは、プログラムへの理解を深めるのにも役立つ。

密な情報交換が物事を成功へ導く

社会人になりたての頃は、「報連相をしっかりやれ」などと叩き込まれるものだ。やがて出世し、部下を束ねる立場になれば、組織内の情報交換を活発化することが上手な組織運営に繋がることを知る。仕事に限った話ではない。今世の中で、多くの人々に受け入れられている有料・無料さまざまな商品やサービスは、売り手と買い手、あるいは買い手同士の情報交換の促進に成功しているものが多い。

こうしてみると、情報交換の促進が物事を成功へ導く鍵の1つになっているように見受けられるが、恐らくプログラミングにおいても成り立つのだろう。

コンピュータを、プログラマと一緒に仕事をこなす仲間であると考えれば、そんな彼あるいは彼女との情報交換は密に行うべきといえる。その手段が、コーディングでありデバッグなのである。

USP MAGAZINE 2014 winter 松浦智之著、「第九回 ユニケージエンジニアの作法」より加筆修正後転載

Last modified: 2014-01-13 00:00:00