段階的詳細化からコードへの変換¶
課題プログラムの理解(3) : 段階的詳細化とコードへの変換¶
トップダウンとボトムアップ¶
プログラムを構成する方法として,大きく分けて,以下の2通りがあります.
- 段階的詳細化 (トップダウン)
プログラム全体の流れを記述し,次第に内部を詳細化していく手法.
- ボトムアップ
プログラムに必要な部品から組み立てていく手法.
通常は,どちらかの手法だけでプログラムを作るのではなく,両方向からのアプローチをバランスよく実践する必要があります.なぜなら,どちらを採っても,以下の不安があるからです.
ボトムアップでは,作成初期段階で必要なプログラム部品を推測するのは難しい.
トップダウンでは,ある程度先の段階まで動作するプログラムを作れないので,完成が危険な賭けになる.
そこで,両者の切替えをどう見切るかが,プログラマの手腕を発揮する部分であるともいえます.本演習では,段階的詳細化によって,プログラムの流れをある程度整理できたら,流れを細かい支流に整理して,支流毎に1つプログラムを書いてみて,動作の確認をしていきます.そうして,プログラムの流れと部品の整備を漸進的に進めていきます.
プログラム全体の流れを書いてみよう¶
プログラム全体の流れを段階的詳細化を使って記述してみよう.
まずは,メインの処理から.
/*** main ***/
while (標準入力から1行読み込む → 成功) {
読み込んだ行を処理をする;
} // 繰り返し
かなり曖昧な感じですね.どの程度詳細に(あるいは,抽象的に)書けばいいのでしょうか.1つの指針として,流れを書く場合は,制御構造 (if/switch, while/for)を入れて「支流」を数個作ることを意識します.詳細化が進み,支流が細くなってきた時点で,短い関数として実装できそうか判断します.
では,もう一段階詳細化してみます. ここでは,処理の粒度が大きそうな,読み込んだ行を処理をする;について詳細化してみましょう.
/**** 読み込んだ行を処理する ****/
if (行頭が '%' ではじまっているなら) {
入力文字列をコマンド入力として処理する
} else {
入力文字列をCSV形式の名簿データとして処理する;
}
さらに,もう一段階詳細化してみます.
/***** 入力文字列をコマンド入力として処理する *****/
2文字目のコマンド文字と4文字目以降の引数を取り出す;
コマンド文字と引数で示された処理を実行;
/***** 入力文字列をCSV形式の名簿データとして処理する *****/
行をコンマで5つの文字列に分割;
5つの文字列を項目毎に構造体や配列に保存;
どちらも似たような構成です.
文字列を解釈できるデータとして準備する
準備されたデータを利用してさらなる処理をする
という流れですね.
さて,詳細化の過程で実際のコード(プログラム)に書けそうな所があったら,コードを記述して,動作の確認をしてみるとよいでしょう.既に,
行をコンマで5つの文字列に分割;
標準入力から1行読み込む;
は,前回の練習課題として書いた関数が使えそうです.文字列分割関数split()と,1行入力関数get_line()ですね.
なお,上記の手順を踏まないで,「CSVを使うんだから,コンマで行を分割するんだろうな,だったら,split()から作って実験するか.」と発想して作ってみるのが,ボトムアップ的発想です.
カンが良ければ,結果は同じかもしれません.
段階的詳細化をコードに変換してみよう¶
ここまでで,段階的詳細化によってプログラムの流れを整理しました.c言語のコードとして書き直しながら,さらに洗練させていきましょう.
以下,再掲です.
/*** main ***/ while (標準入力から1行読み込む → 成功) { // → get_line() 読み込んだ行を処理をする; } // 繰り返し /**** 読み込んだ行を処理する ****/ if (行頭が '%' ではじまっているなら) { 文字列をコマンド入力として処理する } else { 文字列をCSV形式の名簿データとして処理する; } /***** 文字列をコマンド入力として処理する *****/ 2文字目のコマンド文字と4文字目以降の引数を取り出す; コマンド文字と引数で示された処理を実行; /***** 文字列をCSV形式の名簿データとして処理する *****/ 行をコンマで5つの文字列に分割; // → split() 5つの文字列を項目毎に構造体や配列に保存;
各処理を関数と見立てて,Cらしく書き直してみましょう.以下のように名前を決めてみます.
項番
処理名称
関数名
1
main
main()2
標準入力から1行読み込む
get_line()3
読み込んだ行を処理をする
parse_line()4
文字列をコマンド入力として処理する
parse_command()5
文字列をCSV形式の名簿データとして処理する
parse_csv()6
コマンド文字と引数で示された処理を実行
exec_command()7
5つの文字列を項目毎に構造体や配列に保存
new_profile()8
行をコンマで5つの文字列に分割
split()を利用parseは,日本語で「構文解析」といいます.(参考:parse - 英辞郎 on the WEB)
構文解析とは,文字列を解析して,文法に合致するかチェックすることです.本課題のプログラムでいえば,文法とは,仕様に合致したCSV書式や%コマンドの書式ですね.
ここでは,
parse_???によって,構文解析と同時に,「内部構造(構造体や配列)への代入」や,「各コマンド(%Qや%P)への引数の中継」も担うことが期待される関数としています.
これで概形はできました.もう少し,関数の関係を見直しつつ,推敲してみましょう.
[項番4, 6]
parse_command()が成すべき処理のほとんどは,exec_command()が司ることになりそうです.parse_command()相当の処理はparse_line()の中で実装してしまうことにしましょう.[項番5, 7, 8]
parse_csv()がなすべき処理は,new_profile()による構造体への代入そのものに見えます.parse_csv()とnew_profile()をまとめて,1つの関数new_profile()にしてしまいましょう.
parse_csv()の意味を残して,new_profile_fromcsv()という名前のほうがよかったかもしれませんが,今のところCSV以外からstruct profileを作る機能がないので,new_profile()としておきます.注釈
考察の参考ここでは,やや相反するポリシーで
parse_???の実装を考えているように見えます.レポートBの執筆に取り掛かるころに,考察を通して振り返ってみると,考察の取っ掛かりがあるかもしれません.
以下は,上記命名に基づいて,プログラムの流れをCらしく書き直した結果です.各関数間で受け渡すべき引数は,今のところ考慮に入れていません.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | exec_command()
{
/* コマンド文字と引数で示された処理を実行 */
}
new_profile()
{
split(); // (splitを利用して) 行をコンマで5つの文字列に分割
/* 5つの文字列を項目毎に構造体や配列に保存 */
}
parse_line()
{
if (/* 行頭が '%' ではじまっているなら */) {
/* parse_command : 文字列をコマンド入力として処理する */
; // 2文字目のコマンド文字と4文字目以降の引数を取り出す
exec_command(); // コマンド文字と引数で示された処理を実行
} else {
/* parse_csv : 文字列をCSV形式の名簿データとして処理する */
new_profile();
}
}
main()
{
while (get_line()) { // get_line() == 1 や get_line() != 0 でも良い
parse_line();
} // 繰り返し
}
|
main()について,実際に実装例を考えてみましょう.これまでの実装を思い出せば,およそ,以下のような関数として書くことになりそうです.
1 2 3 4 5 6 7 8 9 | int main()
{
char line[MAX_LINE_LEN + 1];
while (get_line(line)) {
parse_line(line);
}
return 0;
}
|
parse_line()には,get_line()で読取った行lineが必要ですから,そのまま引数としています.
なお,複数の関数を呼び出す際には,定義をする順序に注意が必要です.ぱっと注意点が思いつかない人は,教科書K&R(例えば,p.32 や p.88)を読んで,復習してください.