1対1でのプログラミングの教育

以前、大人数相手のプログラミング演習をどうするかというエントリを書きましたが、今回は1対1のプログラミング教育(コーディング手法)について書きます。

最近4年生の卒業研究指導のために、主にC言語Java言語でのプログラミングを教える機会がありました。何を教えるべきか、というのは指導対象の相手に依るのですが、何点かのポイントが重要であることを認識したので、書いておきます。

以下の教育内容は、プログラミングの初級者が中級の入り口に立つために必要な事という観点でまとめてみました。どのような人を対象としているかというと、
C言語Java言語のプログラミングは学んだ
・ついついmain関数や1つの関数の中に、全ての処理を突っ込んでしまう
・期待通りの動作をしないときに、どのようにデバッグしたら良いかわからん
という段階の人です。

<教育内容>
1、常に全体の動作が分かるように、コードを改善する。
2、適切にモジュール分割したコードを書く。
3、期待通りに動かないときは、状態を可視化するためのコードを書く。
4、修正・確認のサイクル(TAT)が速く回るように、コードを改善する。
5、入力・出力・期待値を用いたテストを行うコードを書く。

全ての教育内容で、「コードを書く・改善する」という形式にまとめています。
学生のプログラミングに対する考え方を矯正するのは、なかなか効果が見えないので、コードという形で教育の成果がアウトプットできるように、結果をコードに示すようにしました。

この内容を理解して使える段階に達すると、大幅にプログラミングが楽しくなってきます。ですので、学生の皆さんには是非これらのプログラミングスキルを身につけてほしい、と願います。

それでは、一つずつ説明します。


1、常に全体の動作が分かるように、コードを改善する。

プログラミングの経験が少なく、あまり大きなプログラムを書いたことが無い人の場合、「ついついmain関数や1つの関数の中に、全ての処理を突っ込んでしまう」というプログラムになりがちです。

その場合、自分の書いたプログラムを他人に説明するのも困難ですし、しばらくすると自分の書いたコードの意味するところが分からなくなってきます。すると、機能追加の必要があってプログラムの修正やデバッグをしようとしても、出来なくなってきます。

ですから、パッと見ただけでプログラムの全体の動作(もしくは、プログラムのある部分の動作)が分かるように、常にコードを改善(リファクタリング)し続ける必要があるわけです。

昔から言われている事ですが、基本的には関数を1画面内に収めましょう。そうすると、フローチャートにした場合も1画面に収まります。するとPPTスライド等で他人に説明することが出来るようになります。1画面に収まるサイズが、人間が「全体を把握する事の出来る限界」と思って良いです。

その為には、処理をモジュール(C言語であれば関数・ファイル)に分ける必要があります。
ある関数の中での、処理のまとまりの単位で、更に小さい関数を作り、それを呼び出す形にするという事です。

初学者は、「モジュールに切り分けるためのC言語の文法」をあまり知らないことが多いので、その辺を次項で説明します。
#なお、この辺のモジュールへの切り分けは、Javaであればかなり簡単になります。


2、適切にモジュール分割したコードを書く。

C言語でのモジュールは、関数・ファイルという2つのレベルがあります。

まず第1のレベルは関数単位のモジュールです。
前項で説明したように、最初はmain関数の中だけで完結していた処理も、長くなってくると関数を切り出す必要があります。以下にコード例を示します。

ファイル名:main1.c

int main() {
 処理1
 if(xxx) {
  処理2
 }
 処理3
}


ファイル名:main2.c

void func1() {
処理1
}
void func2() {
処理2
}
void func3() {
処理3
}

int main() {
 func1();
 if (xxx) {
  func2();
 }
 func3();
}

main1.cのコードは数画面に及ぶmain関数だと思ってください。これをmain2.cのコードの様に書き換えると、処理1・処理2・処理3の意味合いが良く分かるようになります。

この際に問題になるのは、func1・func2・func3が、それぞれ共通の変数を使う場合でしょう。
その場合C言語では、グローバル変数を使うのが最も直観にあった解決法だと思います。
以下の、main3.cではcommon_dataというグローバル変数を宣言して、各関数内で使用することにしています。

ファイル名:main3.c

int common_data[100];

void func1() {
処理1(common_dataを使う)
}
void func2() {
処理2(common_dataを使う)
}
void func3() {
処理3(common_dataを使う)
}

int main() {
 func1();
 if (xxx) {
  func2();
 }
 func3();
}

大事なことは、「処理を関数単位に分解して全体の処理を把握できるようにする」という事です。そのために、グローバル変数を用いても良いので、処理をまとまりとしてとらえるようにするようにしましょう。

但し、グローバル変数というのは、より大規模なプログラムや、マルチスレッド・マルチコア向けの並列プログラムを作成する際には問題となることがある、という事は気にしておいてください。これは、中級者から上級者に脱皮する際に、よく勉強しなければならない事です。

第2のレベル(ファイル単位のモジュール)は、1つのファイルが数100行を超えたあたりから、検討する必要があると思います。つまり、ソースファイルを編集していて、どこに何の関数が書いてあるのか、把握不能になるコード量になってきたら、ファイルの分割を考えた方が良いです。また、中級者以降になると、自然と最初からファイル単位でのモジュール化を意識して書くようになってきます。

最初にJavaにおけるファイル単位のモジュール化について説明します。Javaでは1つのクラスは1つのjavaファイルに対応させるという原則があります。これにより、クラスとファイルとの関係が明瞭になっています。

一方C言語では、クラスという単位が無く、またどの程度の量を1つのファイルに記述するかという指針が無いため、混沌としがちです。

C言語でのファイル単位でのモジュール化について、私のお勧めは、クラスに相当する「処理やデータの1つのまとまり」を、1つのファイルのペアにまとめることです。
ファイルのペア、と言っているのは、ソースファイルとヘッダファイルのペア、という事です。

この際注意すべきなのは、ヘッダファイルには変数の実体を書かない事です。
ヘッダファイルは複数のソースファイルからインクルードされる宿命があるので、そこに実体があると、最後にリンクする際に、複数のオブジェクトファイル内に同じ変数の実体が存在するために重複してしまい、問題が生じます。その為、ヘッダファイルには、変数の実体を書かずに宣言のみをするようにします。

グローバル変数を、複数のソースファイルから共通に使用したいのであれば、ヘッダファイルにexternで宣言しておき、どこか一つのソースファイルに変数の実体を作ることで、別のファイルの関数からでもグローバル変数にアクセス可能になります。グローバル変数はなるべく使用を避けた方が良いのですが、C言語ではグローバル変数を用いて書くことが自然である場合が多いので、問題があることは認識しつつ使用可能であることを示しておきます。

なお「リンク」とか「オブジェクトファイル」といった用語が出てきましたが、ファイル単位でのモジュール化を使いこなすためには、どのように実行可能形式のファイルが出来上がるか、という過程を理解しておく必要があります。

以下は、開発環境がVisualStudioでもGCCでも共通の考え方です。
コンパイル:1つのソースファイルから、1つのオブジェクトファイルを作る
リンク:複数のオブジェクトファイルから、1つの実行可能ファイルを作る

コンパイルはソースファイル単位で行われ、オブジェクトファイルが作られます。オブジェクトファイルの中には、関数や変数の名前・型情報と先頭アドレスのリスト、および関数単位で実行可能なコードが格納されています。リンクする際に、複数のオブジェクトファイルで同じ名前の変数や関数が見つかると、重複してしまい正常にリンク不可能です。ですので、各オブジェクトファイルにどの関数・変数が格納されるのか、よく意識して、ソースファイルとヘッダファイルへの配置を考える必要があります。そのための指針が、前述したように、クラスに相当する「処理やデータの1つのまとまり」を、1つのファイルのペアにまとめる、という事になります。

なお、複数のソースファイル・オブジェクトファイルを用いて、実行可能ファイルを作成する手順のことを、「分割コンパイル」といいます。ここで分割コンパイルの手順について詳しく説明することはしませんが、自分が見ても他人が見てもプログラムが理解可能な状態に保つためには、関数単位・ファイル単位でのモジュール化が必須であるという事を理解し、実践していくことをお勧めします。

なお、C言語でのファイル単位のモジュール分割を用いて、オブジェクト指向の設計をC言語マッピングする方法については、以下の書籍が詳しいです。
http://www.cqpub.co.jp/hanbai/books/33/33391.htm


3、期待通りに動かないときは、状態を可視化するためのコードを書く。

期待通りに動かない時に、ソースコードを眺めながら、なんでだろ〜なんで動かないんだろう〜と時間が経過していくという事が、ありがちな風景です。VisualStudioやgdb等のデバッガを使うと、ソースコードを見て1行ずつ実行を進めながら、変数の値の変化を見て確かめることが出来ます。しかし、確認する対象が広範囲になってくると、局所的に変数を見ているだけでは不具合の原因が、なかなか分かりません。

そういう時には、現在の状態を分かりやすく表示するプログラムを作る、ということをすると、一気に目の前の霧が晴れる、という事が良くあります。シミュレーション対象の全体の状態の概要を出力する、とか、あるモジュールに着目して時系列で全ての内部状態を出力する、とか、デバッグの目的に合わせたプログラムを作るのです。

この際、本来のプログラムの中にprintfをいっぱい埋め込んでしまうと、雑然としてしまい、関数あたりの行数も伸びてしまいます。そのため、なるべくデバッグ用の観測コードと、本来のプログラムのロジックを分離するのが良い習慣だと思います。

基本的にはデバッグのための状態表示の関数を作って、調べたいところで呼び出すように作ると、デバッグ版と最終版(リリース版)を上手く分離することが出来るようになります。やり方は色々ですので、各自の好みに応じてで構わないと思います。


4、修正・確認のサイクル(TAT)が速く回るように、コードを改善する。

状態を可視化して、プログラムの何が問題なのかを分かりやすくできたとして、次に問題になるのが、プログラムの実行時間やコンパイル時間です。

バグの原因究明のためには、考えるための材料が必要です。前項で説明した、「状態の可視化」は、そのための材料を揃えるものです。デバッガでのステップ実行に頼っていると、時間がかかる上に同じ状態を実現するのに時間がかかりますので、デバッグしたい状態をなるべく早く再現するためのコードを一生懸命書くことで、結局はデバッグにかかる時間を短くすることが出来るようになります。

更に、以下の項目について考慮することで修正・確認のサイクル時間(TAT: Turn Around Time)を短くできますので試してみてください。

・問題のサイズをパラメータ化する
大規模なデータを扱うプログラムの場合、プログラムの最初に入力データを読み込むだけでかなりの時間を費やしてしまう事は良くあります。最初は小さい入力データで試してデバッグを十分行い、その後、実際に使用する入力データを用いた処理をするという手順を踏むことが、結局はトータルの開発時間を短くすることにつながることを、私たちは経験上知っています。

サイズをパラメータ化して、多少入力データのサイズが変わっても対応できるようなコードを作成することに時間を使いましょう。そうすることで、作成したプログラムを色々な用途や入力データに対応して使用できるようになります。

問題のサイズとして「0・1・2・3」ぐらいを試してあれば、問題のサイズが「いっぱい」の場合も大抵動きます。なお、問題のサイズが「いっぱい」である際に生じる問題については、大抵がメモリ不足が原因だったりします。


・単純作業の自動化(Makefileの活用)
コンパイルしてリンクして実行するという一連の作業を、毎回コマンドを打って行っている人は、Makefileを使うことを検討しましょう。コンパイル・リンク以外の単純作業もMakefileを使うと自動化することが出来ます。

VisualStudioであれば自動化されていますが、その分中身が見えませんので、コンパイル・リンク以外の単純作業の自動化は、少し難しいかもしれません。

なんにせよ、単純作業はなるべくコンピュータにやらせるのが大事です。

実験で少しずつ条件を変えてシミュレーションをしたい場合であれば、以下の様な手段で、コンピュータに自動的に実験をやらせましょう。
・条件をコマンドラインパラメータとして与えるようにプログラムを作成
シェルスクリプト(もしくはバッチファイル)で、順次入力データやコマンドラインパラメータを変更して実行する


5、入力・出力・期待値を用いたテストを行う。

以前のエントリで進捗報告の仕方について書きましたが、プログラムの開発の進捗を測る方法は、テストの結果、で示すのが一番です。一番の意味は、開発の進度を最も適切に表すという意味と、聞いている人が納得しやすい、という意味です。

入力とは、プログラムに与える入力のデータです。
出力とは、プログラムから出力される結果のデータです。
期待値とは、プログラムに出力して欲しい、正しい結果のデータです。

結果のデータを得るためには、状態を可視化するコードが必要です。すなわちテストをするためには、状態を可視化するコードを一生懸命書く必要があります。

そして、出力と期待値の一致比較を、まずは目視で行いますが、次第にプログラムにやらせるようにします(diffコマンドでも構いません)。

プログラムを改善・修正していると、以前通過したテストが再びNGになることがあります。テストを手動で行っていると、以前のテストは段々と面倒くさくなって実施しなくなります。
そのため、テストを自動化し、常にすべてのテストが通過することを確認しながら開発を進めることが、トータルとしての開発期間を短縮することにつながります。

***

私はXP(Extreme Programming)の思想には非常に共感するところがあります。そのためコーディング教育は、XPを指向したものになっていると思います。ただしXPは中級〜上級者向けで、やや洗練されすぎておりプログラミングの初学者にはとっつきにくい所がありますので、なるべく噛み砕いて説明しました。

前回エントリ・本エントリで説明した5項目は、基本的には1人でやるプラクティス(習慣)です。XPは2人でやるものですが、その前提にあたるものであると位置づければ良いかな、と思います。この他に、タスク管理・バージョン管理システムを導入する、という指導もしていますが、おいおい書いていきます。

***

私が教員として1年ぐらい過ごして感じた事は、「プログラミングは苦手」という意識でいる情報工学科の学生が多いことです。何とかして、「プログラミングは楽しい」と感じられる様にしてやりたいのですが。このエントリで書いたようなことが意識できるレベル(中級者)になれば、「プログラミングは楽しい」となりそうなのですが、1対多数の演習講義では、指導するのが難しいです。

つまり、現状の1対多数の演習講義では、プログラムの枠組みがあって、そこを埋めるというスタイルで演習を進めています。そのようなスタイルだと、自分で枠組みを決めていく(関数分割であったり)という訓練が出来ないのです。

そして枠組みを学生に考えさせると、その枠組みが「良い」か「悪い」かを判断する必要があります。枠組みは設計なので、なかなか一概に「良い」か「悪い」かを評価できるものではない、というのも教えづらいポイントです。基本的には、期限内に要求通りに動くコードが書ければ「良い」のですが、明らかに「悪い」設計については、設計レベルでもっと良い方法というのを教員が示してやる必要があります。

学生が作ってきたプログラムに対して、一例ずつ、より良い設計を考える、というのは時間がかかるものですので、少人数のPBL(Project Based Learning)にせざるを得ません。つまり、結局はシステム開発の会社での新人研修・OJTみたいなものになります。PBLの問題は、教える人によるばらつきが大きいことかな、と思います。しかしながら、問題はあるにせよ、学生が自分で枠組みを考えて設計する、ということは早めにやることが必要ですね。