見出し画像

Retro-gaming and so on

RE: Racketで何かゲームらしきものをつくれ!007 アイテム編2

いやあ、相変わらず星田さんが楽しそうだ。

さて、今回やってるプログラム。ロジックは全然問題がない。
そこで、今回はユーティリティの作り方、と言った趣でちょっとした考え方を紹介しよう。

前に書いたが、実はユーティリティを作る、ってのは意外と難しい。
大体、ユーティリティを作ろう、と言うモティベーションはいつ起こるのか。
通常、「何度も違うプログラムを書いてきた」際に、「同様の関数を何度も作らなきゃならない」ハメに陥った時に「要ユーティリティ化を発見する」と言ったカンジにならないと、そのモティベーション自体が出てこない。
例えば高階関数reduceの存在。これが登場した背景も、言っちゃえば「何度も同じ再帰パターンを書かなきゃならなかった」から抽象化としてその「パターン」を取り出した、と言っていいだろう。
じゃなければ、結果、何度も同じパターンをプログラミングせなアカンくなるからだ。
このように、ユーティリティ作成のキッカケ、ってぇのはそれまでに書いたプログラム量に比例する。いきなりプログラミング初学者が、「汎用的なユーティリティを作成しよう」と思えないのは当然、って言えば当然なんだ。

いずれにせよ、「汎用的なユーティリティ作成」ってのはプログラミング初心者には難しい。
一方、ある程度複数言語を知ってる場合、この「ユーティリティ作成」は敷居が下がる。
と言うのも、「ある言語に欠けてて、他の言語で提供されてるある機能」を移植すれば、それはすなわち、「汎用性のあるユーティリティ」になる場合が多いから、だ。
Scheme/Racketも別に、ライブラリ関数で考えると完璧な言語じゃない。
よって、他言語で提供してるユーティリティで、便利に思えそうなモノはガンガン移植し「自身が使ってるプログラミング言語」を強化していって構わない。

Lispではユーザに独自のオペレータを定義する自由があるので, Lispを必要なプログラミング言語にきっちり仕立てることができる. ユーザがテキストエディタのプログラムを書いているのなら, Lispをテキストエディタを書くための言語に変えることができる. またCADソフトのプログラムを書いているのなら, LispをCADソフトを書くための言語に変えることもできる. そしてどんなプログラムを書くかまだ確かでないなら,Lispで書いておくのが安全な賭けだ. それがどんな種類のプログラムになったとしても,それを書いている間に, Lispはその種類のプログラムを書くためのプログラミング言語に進化していることだろう. 

ってヤツだ。
今回は、星田さんも知っているPythonを参考にしよう。
星田さんは「Pythonを忘れちまった」的な事を書いてたが、全然構わない。
と言うのも実の話、「一回でも触った/使った事がある」って経験は思ったより大きいんだわ。「何も知らない」人と「一回でも触った/使った経験がある」人との差はホンマ、メチャクチャ大きい。もちろん「曲りなりにもプログラムを書いた事がある」ってのが要条件ではあるが。
ではちょっと見ていってみよう。

 まずテスト気味にショップの品揃えを抜き出す部分を作る。これをFormatに読ませて表示する部分を作らないといけないが・・選択肢にするために
 「1:防瘴マスク」
 という風にしたい。

うん。
これは何度か書いてるけど、例えばリストがあっても、モダンな言語は、「要素番号を使わなくても済むような」デザインになっている。
逆に言うと、レアではあるけど「要素番号」が使えそうなケースの時に困る事があるんだ。
これはその「レアなケース」だ。

ところで、例えばPythonだと、

class item(object):
 def __init__(self, name, cost, page, times):
  self.name = name
  self.cost = cost
  self.page = page
  self.times = times

とアイテム構造体を作って(※1)、次のようなリストを作る。

lst = [item("防瘴マスク", 10, 26, 1),
  item("防瘴ケース", 15, 26, 1),
  item("光弾", 4, 26, 1),
  item("ロープ", 5, 26, 1)]

そして、これから「1:防瘴マスク」と言うような文字列データのリストを一気に生成したい場合、Pythonだと次のように書くだろう。



そう、ここでのキーポイントはenumerateと言うPython組み込みの関数だ。
こいつはユーティリティとしてRacketに移植する価値がある。

Pythonのenumerateの定義は次のようになってる模様だ。


これをバカ正直に実装する必要はないけど、いずれにせよ、定義式は参考にはなるだろう(※2)。
ここでは関数enumerateを次のようにして実装してみる。

(define enumerate
 (case-lambda
  ((seq) (enumerate seq 0))
  ((seq start) (map (lambda (x y)
           (cons x y))
          (range start (+ (length seq) start))
          seq))))

そうすれば、Pythonと同様にして(と言うか、match-lambdaを利用してもうちょっとカッコ良く)関数show-zaikoの心臓部を書く事が出来る。
つまり、


こういう定義に対して、



となる。
結果、

(define (show-zaiko page)
 (display
  (map (match-lambda (`(, index . ,id)
            (format "~a:~a" index (item-name id))))
  (enumerate (page-lst page) 1))))

と書く事が出来る(※3)。



 ま、これで一応の狙った動きはするんだけど・・なんかまたPrintとEvalとLoopをまとめてしまうクセが出る。Cametanさんの石取りを参考にすると入力部分はInputとして独立させてReadに含めるべきか・・(※4)

Pythonのinputはプロンプトを指定して出力も含めたカタチで提供されている。
厳密に言うと、REPLに於いて、読み込みに出力を含めるべきかどうか、ってのは悩ましいトコだ。
実用Common Lisp(PAIP)でも、REPLの雛形では、プロンプト出力は別個として出力をしていた。

(defun プログラム ()
 (loop
  (print プロンプト)
  (print (変換 (read)))))

ただし、プロンプト出力を入力関数に含める、と言うのはPythonの慧眼で、何より便利だ。ここで言う便利、と言うのは要するに、よりシンプルに記述可能だ、と言う意味になる。
これは真似すべきであり、当然ユーティリティ化する価値がある。



注意点としては、Pythonのinputはあくまで端末に於いてでの文字列のやり取りになる。
一方、Lisp系のreadと言うのはS式なら何でもそのまま解釈してくれる構文解析器付きの読み込み器だ。
この辺、どう実装するか、ってのは悩ましいトコだけど、ここでは一応、Pythonの挙動に合わせた実装をしてみよう。と言う事は、readの代わりにread-lineを使う。

(define input
 (case-lambda
  (() (input ""))
  ((prompt) (display prompt)
       (read-line))))



ゲームの場合も、剥き出しのreadをユーザに触らせるよりも、入力を一旦文字列として受け取ってそれを解析した方が、一見手間だが安全性は向上する。文字列が一種、防御壁の役割を担ってくれるんだ(※5)。

; ショップINPUT関数
(define (shop-input env)
 ;;; 本当は、入力が数値かどうかのチェックが要ると思う。
 (let ((num (string->number
      (input (string-append (cdr assq 'kaukane *shop-messages*)
                "\n"
                (show-zaiko (shop-page env))
                "\n")))))
  (cond ((> num (length (page-lst (shop-page env)))) (shop-input env))
     ((zero? num) (shop-input env))
     (else (display env)))))

さて、Pythonを参考にしてユーティリティ関数を二点作ってみた。
ところで、こういうユーティリティ関数を書いたファイルを、一体どこにどのように置いておくのか、と言う問題がある。
単純には、自分の作業フォルダ内に、例えばutilとでも名付けたフォルダを作ってぶち込んでおけばいい。
そして、例えばenumerate関数を記述したenumerate.rktと言うファイルをutilフォルダ内に作ったとして、そのファイルのアタマの方に次のようなオマジナイを記述しておく。

(provide enumerate)

これはRacketでのライブラリ読み込み、requireの対になるオマジナイだ。



今、ナウシカのゲームを作ってる作業フォルダ内に、プログラムnausicaa.rktがあるとしよう。同階層にユーティリティを詰め込んだutilフォルダがあって、その中にenumerate.rktやinput.rktが入っている。
プログラムnausicaa.rktからenumerate関数を呼び出したい。その場合、nausicaa.rktの冒頭に次のように記述する(※6)。

(require "util/enumerate.rkt")

そうすれば、util/enumerate.rkt側からprovideで指定された関数を呼び出す事が出来る。

とまぁ、これが自作ユーティリティの作り方、の第一歩で、かつ、「自作ユーティリティの育て方」だ。
ユーティリティ用のフォルダを作っておいて、ユーティリティに育てられそうな関数/ファイルは特定のファイルにコピペして、provideの力を借りてガンガンと使いまわすようにしよう。

※1: 厳密に言うと当然オブジェクト指向のクラスだ。
ただし、以前にも書いたが、メソッドを実装してないクラスは事実上、構造体として使う事が出来る。

※2: Pythonのenumerateはジェネレータとしてイテラブルを返す設計になってるが、そこまで真似せんでもいいだろう。

※3: ここで分かってほしいのは、別に星田さんの書いたshow-zaikoが間違ってる、と言う話をしてる事ではない、と言う事だ。
むしろ、気づくだろうが、事実上、星田さんが書いたshow-zaikoenumerateを「中に含んでる」。逆に言うと、Pythonのenumerateは星田さんが書いたshow-zaikoから「一部を取り出したモノ」と言って良く、それが故にユーティリティなんだ。
この「自然と中にenumerateを含んでる」パターンは、今後もプログラミングを続ける際に「良く見かける」パターンとなっていくだろう。
だからこそ「切り出す価値」が生じる。

※4: 実際このプログラムがルーピングしてるのはエラーチェックが目的だから、だ。この辺、エラーチェックを「ここでマジメにやる」か、あるいは入力が望ましくない場合、敢えての例外を投げて、大枠のREPLでその例外を受けて例外処理にかけるか、と言うのは、今の時点では趣味の問題となる。
いずれにせよ、その辺はあとで直せるだろう。
ちなみに、REPLらしく見えちゃう原因は、本体のelse節がdisplayしてるせいだ。
そこは単純にenvを返しちゃえば単純な「エラーチェック機構付きの」evalと言って良いだろう。

※5: この辺は、実はいつぞや話した「ネット上で何故にREADを晒さないか」と言う話と同じだ。
例えばREADを剥き出しにしちゃうと、それを使ってLispで「ゲームと全然関係ない事」を実行出来る可能性が常にある。入力がS式を「認める」と言うのはそれくらい強力な事ではある(例えばゲームの筈なのに入力画面で電卓的な四則演算が出来たりconsしたりcarしたりcdrしたり・・・とか言う可能性がある、って事だ)。
もちろん、ローカル環境で実行する以上そこまで気にせんでいいし、実際問題ゲーム用evalで入力に制限付けて振り分ける事になるだろうが、いずれにせよ、例えば端末経由での入力情報が、何故に全部文字列になって返ってくるのか、と言うのもこの「セキュリティの為」って理由が一番大きい。文字列は解析しないとコマンドとして成り立たないわけで、その「ひと手間」がコンピュータが入力を直接実行しないように出来る防御壁の役割になっているんだ。

※6: 結局、Racket組み込みのSRFIのような外部ライブラリ(ヘンな表現だが)はrequire + シンボルで呼び出せるが、一方、自作ライブラリに関してはrequire + "ファイルへの相対パス"、とパスを文字列にしないといけない。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「RE: Racketで何か」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事