10 月 28 日(金),31 日(月)

手続き制御の実装

タートルグラフィックスのスクリプト(命令ファイル)について, より「プログラミング言語」らしくなるように, 手続き制御コマンドを実装しよう.


ファイルアクセス

基本的に,ファイル入出力処理では, データは,ファイルの先頭から末尾の方向へ順番に連続的に読み書きされる. このような方式は, シーケンシャルアクセス(sequential access)と呼ばれている.

これまでに利用してきた入出力関数 fgets( )scanf( )printf( ), 等だけによるファイル処理は,シーケンシャルアクセス.

一方,ファイル内でのデータの読み書き位置が不連続的な入出力の方式は, ランダムアクセス(random access)と呼ばれている. これはデータを再読み込みしたり,読み飛ばしたりする場合に必要となる.

現在開発中の tg では,実は既に,ランダムアクセスも利用している.

Cの標準ライブラリには, ランダムアクセスを実現するための ファイル位置関数が用意されている:

昭和な解説: 「ファイルとはカセットテープのようなもの」 と考えると理解しやすいんだが... 知ってますか?

これらの関数についての詳しくは, K&R p.311 を参照せよ.


repeat 〜 end

前回示した基本プログラム tg.c には, 反復のためのコマンド repeat 〜 end が 既に実装されている. このコマンドの仕組としては, List 1 の通り, すでに実行した命令をスクリプトファイルから再度読み込みながら, 自分自身 Tg( ) を再帰的に呼び出すようになっている.

List 1. tg.c での repeat 〜 end の実装例
void Tg(Turtle *t, FILE *fp, Pbm *pbm)
{
	...

	while (1) {
	  ...

	  // 他のコマンドの処理
	  ...

		else if (strcmp(cmd, "repeat") == 0) {
			if (narg < 1) break;
			nrep = atoi(arg[0]);
			fpos = ftell(fp);			// 現在のファイル位置を記録
			for (i = 0; i < nrep; i++) {
				fseek(fp, fpos, SEEK_SET);	// 記録されたファイル位置へ移動
				Tg(t, fp, pbm);			// 再帰呼び出し
			}
		}
		else if (strcmp(cmd, "end") == 0) {
			return;
		}
	}
}								

List 1 を理解するための例として, 次のスクリプト dash.tg の実行手順を考えよう:

# 実線を描く
 1: pendown
 2: forward 100

# 隙間を開ける
 3: penup
 4: right 90
 5: forward 20
 6: right 90

# 破線を描く
 7: repeat 10
 8:   pendown
 9:   forward 6
10:   penup
11:   forward 4
12: end

# EOF
このスクリプトでは,実線と破線を描く. なお,行番号は説明のためだけのものであり, 実際のスクリプトでは行番号は書き込まれていない.

このスクリプト dash.tg に対する プログラム tg.c の動作は次の通り:

言語処理プログラムの動作を理解するには,このように, 入力ファイル(スクリプト)の進み方と プログラム(ソース)の進み方の両方に気を配る必要がある.

また特に,end コマンドの処理がわかりづらいだろう. Fig.1 を見よう. このコマンドによる return では, Tg( ) の再帰呼び出しの直後へ戻ることになる. そう,main( ) へ戻るわけではない.

Fig.1. repeat 〜 end の処理手順

push 〜 pop

分岐のためのコマンド push 〜 popについて説明する. これは,分岐といっても, 処理を分岐(if 文のように命令を選択的に実行)するのではなく, 線を分岐するためのものだ.

このコマンドは未実装.本日の課題.

たとえば,次のスクリプトでは,文字 Y のような図形を描く:

forward 10	# 根元を描く

push		# 分岐点をおぼえておく
left 45		# 左へ分岐
forward 10

pop		# 分岐点に戻る
right 45	# 右へ分岐
forward 10

この機能は,次のアルゴリズムによって実現できる:

ここで,「タートルの状態」とは,タートルの位置・方向・等の情報であり, 要するに,Turtle 構造体の値のことだ.

ところで,「push」はスタックに関連する専門用語であるが, アルゴリズムを真面目に勉強した人は疑問に思うことだろう, 「スタックのデータ構造を作る必要はないのか?」と. Cでは,関数内の自動変数それ自体がスタック要素になっているので, 再帰呼び出しを利用する場合には, わざわざスタックのデータ構造を作る必要はない.

要するに,このプログラムは かなり手抜きして合理的に作られている.

なお,再帰呼び出しを使わない場合には, 自前でスタックのデータ構造を作る必要がある.

ちなみに,スタックを使わずに分岐することも可能であるが, 次のように,分岐点に戻る処理が面倒である:

forward 10	# 根元を描く

left 45		# 左へ分岐
forward 10

right 180	# 分岐点に戻る
forward 10
left 135

right 45	# 右へ分岐
forward 10

ちなみに,スタックを使わないで分岐すると, 何歩戻ればよいのか?何度戻せばよいのか?ややこしくなる. 二度手間でもあるし...

さらに,分岐の先でさらに分岐...とかの場合, 爆発的に複雑になってしまう.


exec

exec は, 外部手続き(他のスクリプトファイル)を実行するためのコマンドである. つまり,関数呼び出しみたいなものだ.

このコマンドも未実装.本日の課題.

たとえば,四角形を描くスクリプト square.tg

repeat 4
  forward 50
  right 90
end

を用意しておけば,短いスクリプト squares.tg

goto 300 150
exec square.tg		# 四角形
goto 100 150
exec square.tg		# 四角形

を書くだけで複数の四角形を手軽に描ける. この機能も,再帰呼び出しを使えば,簡単に実現できる.

引数を指定できるようにすれば, たとえば,大きさの異なる同じ図形を描けるとか, もっと便利になるだろう. (上級者向け課題. たとえば,exec square.tg 0.5 と書けば, 半分の大きさの四角形が描かれるように.)

なお,この機能を使わないと,次のようにスクリプトが冗長になってしまう:

goto 300 150
repeat 4		# 四角形
  forward 50
  right 90
end
goto 100 150
repeat 4		# 四角形
  forward 50
  right 90
end
この例のように四角形ならば大した手間でもないかもしれない. しかし,もっと複雑な図形をたくさん描きたい場合, スクリプト作成の手間が爆発的に増えてしまうだろう.

本日の課題

  1. pushpop コマンドと exec コマンドの 一方または両方を実装せよ.
  2. 改造したソースファイル(tg.c 等)のみをレポートせよ. 再利用している cg と共通のソースファイル・ヘッダファイルについては レポート不要.

    基本的には tg.c のみでよいハズだが, tg で再利用している cg の API 仕様(関数プロトタイプ等)に 独自の改良を施している場合, その改良部分のコードあるいはファイルについても提出せよ.
  3. これらのコマンドを駆使して, オリジナルかつ複雑なグラフィックスを生成するスクリプトを作成せよ.
  4. 作成したスクリプトファイル(*.tg)および画像ファイル(PNG形式)をレポートせよ. 画像ファイルを作るには,convert コマンドを使えばよい:

    $  ./tg  スクリプト.tg  | convert  -  画像.png
    $  display  画像.png
    
    または,display で右クリックして保存(save)とかでもよい.

なお,今回のレポートでは, 画像については添付ファイルとして送信すること. ソースおよびスクリプトについてはメール本文に記述すること.

余裕のある人は,この実験テーマで学んだテクニックを駆使して, グラフィックスに関係したプログラムを自由に作成してみては?

レポート提出 注意事項

上級者向け問題

  1. 変数の機能を実装してみよう.
  2. 使用例:

    set a 10		# 変数 a へ値を代入
    forward $a		# 変数 a の値を利用
    
    「変数名はアルファベット1文字だけに限る」 とか制限すれば作りやすいだろう, たとえば,スクリプトで set a 10 と書いてあったら, プログラムでは var['a'] = 10; とする,みたいな.
  3. サブルーチンの機能を実装してみよう.
  4. 使用例:

    learn polygon {	# サブルーチン polygon の定義
    	repeat $1		# 引数1番目を利用
    		forward $2	# 引数2番目 〃
    		left $3		# 引数3番目 〃
    	end
    }
    
    polygon 3 100 120	# サブルーチンで三角形を描く
    polygon 4 50 90		#    〃   四角形 〃 
    

    exec とは異なり, 同じスクリプトファイル内で別の処理を呼び出すことになる. これはとても難しいかもしれない.

    サブルーチンの名前(文字列)については, 文字数を限定し,文字配列で実現するとよいだろう. また,サブルーチンの中身については, ファイル位置だけを記録しておくようにする. そして,これらを構造体の配列に記録する,みたいな.

    サブルーチンを定義中なのか/呼び出し中なのか状況に応じて, { 〜 } の部分を読み飛ばすか/実行するかの切り替えも必要だろう.

  5. その他,KTurtle を参考にして,機能を拡張してみよう.

以下のヒントを読むと簡単すぎる. まずは,ヒントを読まずに作業してみよう.

push 〜 pop のヒント

次のような感じ...

void Tg(Turtle *t, FILE *fp, Pbm *pbm)
{
	...
	if (... "push" ...) {
		...		// 再帰呼び出し前のタートルの状態 *t をおぼえておく(一時変数へコピー)
		Tg(t, fp, pbm);	// この結果,状態 *t が変わってしまうので...
		...		// おぼえていた状態 *t を思い出す(一時変数からコピー)
	}
	if (... "pop" ...) return;
}

よくあるまちがい:再帰呼び出し前に構造体のポインタ t をコピーしている. ポインタ t をコピーしても無駄だ. 実体 *t をコピー しておく必要がある. 「構造体へのポインタ」と「構造体の実体」とを区別せよ.

なお,変数 t は, 関数 Tg( ) 内では構造体へのポインタだが, 関数 main( ) 内では構造体の実体となっている. 注意せよ.


exec のヒント

たとえば,命令「exec square.tg」の場合, ファイル square.tg を開き, そのファイルに対して Tg() を再帰呼び出しするだけだ.

このコマンドは,機能としては強力(スクリプトを効率的に記述可能)であるが, ソースコードとしては簡単過ぎる... push 〜 pop の方が面白い問題だ. また,上級者は,引数として拡大率を追加してみるとよい.

よくあるまちがい:ファイルを閉じ忘れている. 開いて使い終わったら閉じること.


課題提出完了したら,次の実験テーマの予習として, 昨年度の作品を試してみよう. H27実験I オリジナルゲーム作品集


(c) 2016, yanagawa@kushiro-ct.ac.jp