タートルグラフィックスのスクリプト(命令ファイル)について, より「プログラミング言語」らしくなるように, 手続き制御コマンドを実装しよう.
基本的に,ファイル入出力処理では, データは,ファイルの先頭から末尾の方向へ順番に連続的に読み書きされる. このような方式は, シーケンシャルアクセス(sequential access)と呼ばれている.
一方,ファイル内でのデータの読み書き位置が不連続的な入出力の方式は, ランダムアクセス(random access)と呼ばれている. これはデータを再読み込みしたり,読み飛ばしたりする場合に必要となる.
Cの標準ライブラリには, ランダムアクセスを実現するための ファイル位置関数が用意されている:
これらの関数についての詳しくは, K&R p.311 を参照せよ.
前回示した基本プログラム tg.c には, 反復のためのコマンド repeat 〜 end が 既に実装されている. このコマンドの仕組としては, List 1 の通り, すでに実行した命令をスクリプトファイルから再度読み込みながら, 自分自身 Tg( ) を再帰的に呼び出すようになっている.
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( ) へ戻るわけではない.
分岐のためのコマンド push 〜 popについて説明する. これは,分岐といっても, 処理を分岐(if 文のように命令を選択的に実行)するのではなく, 線を分岐するためのものだ.
たとえば,次のスクリプトでは,文字 Y のような図形を描く:
forward 10 # 根元を描く push # 分岐点をおぼえておく left 45 # 左へ分岐 forward 10 pop # 分岐点に戻る right 45 # 右へ分岐 forward 10
この機能は,次のアルゴリズムによって実現できる:
ところで,「push」はスタックに関連する専門用語であるが, アルゴリズムを真面目に勉強した人は疑問に思うことだろう, 「スタックのデータ構造を作る必要はないのか?」と. Cでは,関数内の自動変数それ自体がスタック要素になっているので, 再帰呼び出しを利用する場合には, わざわざスタックのデータ構造を作る必要はない.
要するに,このプログラムは
かなり手抜きして合理的に作られている.
なお,再帰呼び出しを使わない場合には, 自前でスタックのデータ構造を作る必要がある.
ちなみに,スタックを使わずに分岐することも可能であるが, 次のように,分岐点に戻る処理が面倒である:
forward 10 # 根元を描く left 45 # 左へ分岐 forward 10 right 180 # 分岐点に戻る forward 10 left 135 right 45 # 右へ分岐 forward 10
ちなみに,スタックを使わないで分岐すると, 何歩戻ればよいのか?何度戻せばよいのか?ややこしくなる. 二度手間でもあるし...
さらに,分岐の先でさらに分岐...とかの場合, 爆発的に複雑になってしまう.
exec は, 外部手続き(他のスクリプトファイル)を実行するためのコマンドである. つまり,関数呼び出しみたいなものだ.
たとえば,四角形を描くスクリプト square.tg:
repeat 4 forward 50 right 90 end
を用意しておけば,短いスクリプト squares.tg:
goto 300 150 exec square.tg # 四角形 goto 100 150 exec square.tg # 四角形
を書くだけで複数の四角形を手軽に描ける. この機能も,再帰呼び出しを使えば,簡単に実現できる.
なお,この機能を使わないと,次のようにスクリプトが冗長になってしまう:
goto 300 150 repeat 4 # 四角形 forward 50 right 90 end goto 100 150 repeat 4 # 四角形 forward 50 right 90 end
改造したソースファイル(tg.c 等)のみをレポートせよ. 再利用している cg と共通のソースファイル・ヘッダファイルについては レポート不要.
作成したスクリプトファイル(*.tg)および画像ファイル(PNG形式)をレポートせよ. 画像ファイルを作るには,convert コマンドを使えばよい:
$ ./tg スクリプト.tg | convert - 画像.png $ display 画像.png
なお,今回のレポートでは, 画像については添付ファイルとして送信すること. ソースおよびスクリプトについてはメール本文に記述すること.
余裕のある人は,この実験テーマで学んだテクニックを駆使して, グラフィックスに関係したプログラムを自由に作成してみては?
使用例:
set a 10 # 変数 a へ値を代入 forward $a # 変数 a の値を利用
使用例:
learn polygon { # サブルーチン polygon の定義 repeat $1 # 引数1番目を利用 forward $2 # 引数2番目 〃 left $3 # 引数3番目 〃 end } polygon 3 100 120 # サブルーチンで三角形を描く polygon 4 50 90 # 〃 四角形 〃
exec とは異なり, 同じスクリプトファイル内で別の処理を呼び出すことになる. これはとても難しいかもしれない.
サブルーチンの名前(文字列)については, 文字数を限定し,文字配列で実現するとよいだろう. また,サブルーチンの中身については, ファイル位置だけを記録しておくようにする. そして,これらを構造体の配列に記録する,みたいな.
サブルーチンを定義中なのか/呼び出し中なのか状況に応じて, { 〜 } の部分を読み飛ばすか/実行するかの切り替えも必要だろう.
以下のヒントを読むと簡単すぎる. まずは,ヒントを読まずに作業してみよう.
次のような感じ...
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 square.tg」の場合, ファイル square.tg を開き, そのファイルに対して Tg() を再帰呼び出しするだけだ.
このコマンドは,機能としては強力(スクリプトを効率的に記述可能)であるが, ソースコードとしては簡単過ぎる... push 〜 pop の方が面白い問題だ. また,上級者は,引数として拡大率を追加してみるとよい.
よくあるまちがい:ファイルを閉じ忘れている. 開いて使い終わったら閉じること.
課題提出完了したら,次の実験テーマの予習として, 昨年度の作品を試してみよう. H27実験I オリジナルゲーム作品集
(c) 2016, yanagawa@kushiro-ct.ac.jp