課題
「スクリプト」とは亀に対する指令を独自の文法に従って記述したファイルを意味しています. この課題で作成するプログラムでは,亀にスクリプトを読み込ませてスクリプトの記述に従って,亀を動作させるようにします. つまりプログラム内に亀の動作を(タートルグラフィクスの文法で)直接記述する代わりに,別途用意したスクリプトの記述に従って亀が動作するようにするわけです. スクリプトの仕様は以下で説明する条件のもとで自由に定めて下さい.
このプログラムを実現すれば,プログラム自体を書き換えることなく,別途スクリプトを用意することで,(スクリプトの仕様の範囲で)さまざまな図形を描くことができるようになります.
もくじ
スクリプト
ここではこの課題で実現するスクリプトとはどういうものかを説明します.
スクリプトの例
まずスクリプトの例を示します.
color 255 0 0 go 100 rotate 120 go 100 rotate 120 go 100 rotate 120
この記述が「タートルグラフィクス」での亀の動作を表していると解釈することは容易でしょう. ここで示したスクリプトは次のような仕様に基づいて作成しています.
スクリプトでの命令 | 実行するタートルグラフィクスの処理 |
---|---|
go 距離 | forward(距離) |
rotate 角度 | turn(角度) |
color 赤 緑 青 | set_color(赤,緑,青) |
たとえばスクリプトに「go 100」と書いてあったとすると, 「forward(100)」を実行するわけです.
ここで示したのは飽くまでも一例です. 以下に示す要求事項を満たす限り,どんな機能をどんな形で実現しても構いません.
スクリプトの仕様への要求事項
今回の課題で実現するスクリプトの仕様に求める条件を示します.
- 実現する機能
少なくともタートルグラフィクスの「forward」「turn」「set_color」の機能を実現すること. さらに他の機能を実現することも歓迎する. fill,hover,繰り返し等を実現できると実用性が高まるだろう(実現は簡単ではないだろう). - 描画データの仕様
長さ,角度,色など描画に使うデータはすべて数値のみを扱えばよい.変数,演算処理はスクリプトの機能として実現しなくてもよい(実現するのはかなり大変だろう). - 文法
スクリプトの文法は適宜定めてよい(上の例に従う必要はない). - 例外処理
スクリプトが正しく記述されていない場合の処理(例外処理)は考えなくてもよい(もちろん例外処理を実現してもよい).
[*重要*] スクリプトに関する説明のプログラムへの記述
プログラムで設定したスクリプトの仕様の説明をファイルの冒頭の「=begin〜=end」の間に記述しておいて下さい.
=begin
所属:
氏名:
学生番号:
難易度(5段階評価):
スクリプトの仕様
感想など(任意)
=end
上の例に示したように「スクリプトでの命令」と「実行するタートルグラフィクスの処理」を対応付けた説明を書いておくことを想定しています.
スクリプトの作成
今回はスクリプトの例を実際に作成して,プログラムとともに提出してもらうことを想定しています.
スクリプトは,適当な名前のファイルで作成してください. プログラムと同様に「文字」で構成されるファイル(テキストファイル)として作成することになります.
ファイル名には基本的に拡張子をつける習慣があって, ファイル名の拡張子によって,そのファイルを開くアプリケーションを決めることも少なくありません. しかし本質的には拡張子はファイル名の一部に過ぎません. スクリプトを作成するにあたって,拡張子も含めてファイル名は自由に決めてください.
スクリプトを作成するには, プログラムを作成するのと同じアプリケーションで自由な名前でファイルを新規作成して, 自分で定めた文法に従って命令を記述して,適当な名前で保存すればOKです. どんなファイル名であれ,ファイルがスクリプトの文法に従って記述されていれば,それを読み込むことに問題はありません.
想定する実行方法
今回のプログラムは実行するために「スクリプトのファイル」を指定することになります. 指定の方法は次のいずれかとします.
- ファイル選択ダイアログを使う
- コマンドライン引数を使う
以下,それぞれについて説明します.
ファイル選択ダイアログを使う
プログラム実行中に「ファイル選択ダイアログ」を開いて,スクリプトのファイルを選択させることができます.
require 'gtr/fileselect' # プログラムの先頭にこの行を追加 : : class Turtle def draw ## ファイル選択ダイアログによるファイルの選択 # ファイルが選択された場合はその名前が得られる # キャンセルされた場合はnilが得られる fname = World.select_file
「ファイル選択ダイアログ」はポップアップウインドウでマウスでファイルを選択できるようにするものです. 「ファイル選択ダイアログ」の処理が終わるとファイル名が得られます. それを変数に代入してプログラムで利用できます. 上の例では「fname」に選択されたファイルの名前が代入されます.
なおファイルの選択をキャンセルすることもできます. キャンセルされた場合,ファイル名として「nil」が得られます(nil=何もない). 上の例では「fname」に「nil」が代入されます.
コマンドライン引数を使う
コマンドライン引数(ARGV)でスクリプトのファイルを指定する場合, プログラムは次のように実行することになります. ここではプログラムのファイル名を「gtr_script.rb」として, スクリプトのファイル名を「square.ts」とします.
このときコマンドライン引数「square.ts」は次のようにして取り込むことができます.
class Turtle def draw fname=retrieve() # 記憶させたファイル名を取り出す end end : : t = Turtle.new() # 亀を用意する t.push(*ARGV) # コマンドライン引数を亀に憶えさせる
亀を用意(Turtle.new)した直後に,コマンドライン引数を亀に憶えさせておいて(push), drawメソッドの中でスクリプトのファイル名が必要になった時点で,それをretrieveによって取り出します(上の例ではfnameにファイル名が代入されます).ARGVの前に「*」をつけることに注意して下さい(Rubyでは配列の前に「*」を付けてメソッドの引数に渡すと配列が要素に展開されてメソッドに渡されます).
技術要素
次のようなメソッド等が使えます.必要に応じて使って下さい. 必要でなければ利用しなくても構いません.
- 文字列末尾の「改行」の削除(chomp!)
getsメソッドで読み込んだ文字列の末尾には「改行」があります. chomp!メソッドで改行を取り除くことができます.File.open(fname) do |fin| line = fin.gets() # ここで問題なく1行読み取れたとする line.chomp! # lineの末尾の改行を取り除く end
- 文字列の単語への分解(split)
splitメソッドにより,文字列を(連続した)空白で区切った単語の配列を得ることができる. 生成される配列の各要素はすべて文字列であることに注意せよ. なお文字列の末尾に改行があっても影響はない.str = 'color 224 64 96' str2 = "color 224 64 96\n" words = str.split # words == ["color","224","64","96"] words2 = str2.split # words2 == ["color","224","64","96"]
- 部分配列
配列の一部を取り出すことができる.ary = ['This','is','an','array','that','has','8','items'] # [開始番号..終了番号]で指定(負の番号によって末尾から-1,-2,...と逆順で数えた要素を指定できる) ary[0..2] # ==> ["This","is","an"] ary[1..3] # ==> ["is","an","array"] ary[2..-1] # ==> ["an","array","that","has","8","items"] ary[3..-2] # ==> ["array","that","has","8"] # [開始番号,個数]で指定 ary[0,3] # ==> ["This","is","an"] ary[1,4] # ==> ["is","an","array","that"] ary[2,4] # ==> ["an","array","that","has"]
- 配列の要素の一斉変換(map)
mapメソッドにより,配列の各要素に同一の処理を施して,その結果を新しい配列に格納することができる. 処理はブロックで表現する.各要素をブロックパラメタで表して,処理の方法をブロック内に記述する.src0 = [3, -5, 2, 4, -1] dst00 = src0.map { |u| u+1 } # それぞれに1を加える # dst00 == [4, -4, 3, 5, 0] dst01 = src0.map { |v| v.abs } # それぞれを絶対値に変換 # dst01 == [3, 5, 2, 4, 1] src1 = ['a','b','c'] dst10 = src1.map { |s| s.upcase } # それぞれ大文字に変換 # dst10 == ["A","B","C"] dst11 = src1.map { |t| t.succ } # それぞれ「次の文字」に変換 # dst11 == ["b","c","d"]
- 配列の先頭要素の取り出し(shift)
shiftを使うと配列から先頭の要素を取り出すことができる. 取り出した先頭の要素は配列から削除される. その結果,残りの要素は一つずつ前にずれる.a = [2,3,4,5,6] len = a.size # len == 5 b = a.shift # ==> b == 2, a == [3,4,5,6] len2 = a.size # len2 == 4 (先頭が取り除かれた→要素が1個減った)
- 文字列の数値への変換(to_i,to_f)
文字列にto_iを適用すると,文字列の先頭部分(最初に±があれば,それも合わせて)の数字の列が整数に変換され, to_fを適用すると,(小数点があればその後の数字列も含めて)小数点数に変換される.str0 = '1.2' a = str0.to_i # a == 1 x = str0.to_f # x == 1.2 str1 = '+12.3+4' b = str1.to_i # b == 12 y = str1.to_f # y == 12.3 str2 = '-12.3.4' c = str2.to_i # c == -12 z = str2.to_f # z == -12.3
「to_i」は文字列の先頭から数字列(数字のみが並んでいる部分)を取り出して, 整数に変換する.先頭に数字列がない場合は0に変換する. 「to_f」は文字列の先頭から「数字列」または「数字列.数字列」を取り出して 小数点数に変換する.先頭に数字列がない場合は0.0に変換する. いずれも文字列の先頭が正負の符号(±)の場合はそれも含めて変換する. - メソッドを指定した実行(send)
sendメソッドにより,メソッド名(Symbolか文字列で指定)と引数を指定して,そのメソッドを実行できる. なおメソッドの引数として,配列名の前に「*」を付加しておくことで, 配列を要素に分解して,それぞれを個別に引数として渡すことができる(sendメソッドに限らず,どのメソッドでもこの記法は使える).send(:set_color,0,0,255) # 「set_color(0,0,255)」と同じ.メソッド名「set_color」はSymbolで指定 r = 100; a = 60 a_param = [r,-a,true] send('arc',*a_param) # 「arc(100,-60,true)」と同じ.メソッド名「arc」は文字列で指定
- ハッシュ(Hash)
「キー」と「値」を対応させた構造をもつデータをハッシュ(Hash)という. ハッシュは整数以外の値を添字とする配列のように使える.## (注) 以下の例ではハッシュのキーにSymbolを使っている # ハッシュの定義 wday = {'mon'=>'月','tue'=>'火','wed'=>'水','thu'=>'木','fri'=>'金','sat'=>'土','sun '=> '日'} # ハッシュの要素の参照 wday['sun'] # ==> "日" wday['mon'] # ==> "月" # ハッシュの要素の追加 wday['holiday'] = '祝日' # 追加した要素の参照(最初からある要素と全く同じ) wday['holiday'] # ==> "祝日"
プログラムテンプレート
次に示すプログラムのテンプレート(雛型)を使って下さい. このプログラムは名前を適宜変えた上で保存して利用してください.
サンプルプログラム
サンプルプログラムを示します. プログラムをブラウザの画面で開いたときに文字化けしてしまう場合には, ダウンロードしてEmacs等で開いてみて下さい.
[参考] Symbol
ここではRubyの「Symbol」(シンボル)について簡単に説明します. 課題のためには以下を理解する必要はありません.
Symbolは「文字列」(文字が並んだデータ)と似ていますが,異なる種類のものです. Symbolとは「名前」で区別されるデータです. コロン(:)のあとに「名前」を書いて定義します. なお「:」は名前には含まれません.それにつづく部分が名前です.
:a_symbol # シンボル
上で述べたようにSymbolは文字列(文字の並び)ではありません. 文字列のデータは書き換えられます.一方でSymbolは書き換えられません. 同じ文字の並びで構成される文字列(の実体)はいくつでも作れますが,同一の名前のSymbol(の実体)は一つしかありません.
# # 文字列とそのデータの実体について調べてみる # s = "ace" # 文字列 t = "ace" # 文字列 s == t # ==> true (s,tの値は等しいか?→等しい) s.equal?(t) # ==> false (s,tは同一の実体か?→そうではない.「equal?」はデータの実体の同一性を判定する) # sの1文字目を"A"に書き換える. s[0,1] = "A" # s == "Ace" # sへの変更はtには影響しない.つまりs,tの実体は同じではない.たまたま同じ文字が並んでいただけ. s == "Ace" # ==> true (sの値は"Ace"に等しいか→等しい) t == "ace" # ==> true (tの値は"ace"に等しいか→等しい) s == t # ==> false (s,tの値は等しいか?→等しくない) s.equal?(t) # ==> false (s,tは同一の実体か?→そうではない) # # Symbolとそのデータの実体について調べてみる # x = :ace # Symbol y = :ace # Symbol z = :Ace # Symbol x == y # ==> true (x,yの値は等しいか?→等しい.名前(ace)が同一である) x == z # ==> false (x,zの値は等しいか?→等しくない.名前(ace,Ace)が異なる) x.equal?(y) # ==> true (x,yの実体は同一である) x.equal?(z) # ==> false (x,zの実体は異なる) # # 文字列とそのデータの実体について調べてみる(2) # t = s # tをsと同じ文字列データと対応させる t.equal?(s) # ==> true (s,tは同一の実体である) t[2,1] = "t" # tの3番目の文字を"t"に書き換える(t=="Act") s == "Act" # ==> true (sの値は"Act"に等しいか→等しい)