見出し画像

Retro-gaming and so on

RE: プログラミング日記 2022/03/13~

星田さんの記事に対するコメント。

このプロシージャは間接的に構造体を使ってるからここだけ引数をw以外にしたら動かないだろう!
と、思ったら動く!

動きますね。
別に仮引数名で「判別」してるわけじゃあないんで。
例えば単純な話、big-bangとか構造体に関係なく、

> (define x 1)
> (define (foo y) (+ y 1))
> (foo x)
2
>

として動くでしょ?関数fooの仮引数名をyにしたから、っつっても「xと言う名前じゃないと使えない」ワケじゃないでしょ?
仮引数名は何でも良いのです。

ネットで他にBig-bangについて質問をしてる人がいて、それに対する注釈付きの答えがこちら。いや、これなら全然分かるんだけどなぁ・・

んじゃやってみますか。

#lang racket

(require 2htdp/universe 2htdp/image)

;;; 大域変数

(define TEXT-SIZE 16)
(define TEXT-X 160)
(define TEXT-UPPER-Y 90)
(define TEXT-LOWER-Y 270)
(define WIDTH 640)
(define HEIGHT 360)
(define SIZE 36)

;;; The Data

(struct interval (small big))

(define HELP-TEXT
 (text "↑ for larger numbers, ↓ for smaller ones"
  TEXT-SIZE
  "blue"))

(define HELP-TEXT2
 (text "Press = when your number is guessed; q to quit."
  TEXT-SIZE
  "blue"))

(define COLOR "red")

(define MT-SC
 (place-image/align
  HELP-TEXT TEXT-X TEXT-UPPER-Y "left" "top"
  (place-image/align
   HELP-TEXT2 TEXT-X TEXT-LOWER-Y "left" "bottom"
   (empty-scene WIDTH HEIGHT))))

;;; The Main Function

(define (start lower upper)
 (big-bang (interval lower upper) ; <-- interval 構造体の初期値設定
  (on-key deal-with-guess) ; <-- キーイベント生成
  (to-draw render) ; <-- 画像レンダリング
  (stop-when single? render-last-scene))) ; <-- 終了テスト

;;; Key-Events

(define (deal-with-guess w key)
 ; w は big-bang の環境情報 (interval 構造体)
 ; キーが↑の時、環境情報を bigger へ転送
 (cond ((key=? key "up") (bigger w))
   ; キーが↓の時、環境情報を smaller へ転送
   ((key=? key "down") (smaller w))
   ; キーがqの時、環境情報を stop-with へ転送
   ((key=? key "q") (stop-with w))
   ; キーがqの時、環境情報を stop-with へ転送
   ((key=? key "=") (stop-with w))
   ; それ以外は big-bangに環境情報をそのまま返す(結果何も起こらない)
   (else w)))

(define (smaller w)
 ; big-bang の環境情報を受け取り環境情報を
 ; interval 構造体で組み立て直して返す
 (interval (interval-small w) 
    (max (interval-small w) (sub1 (guess w)))))

(define (bigger w)
 ; big-bang の環境情報を受け取り環境情報を
 ; interval 構造体で組み立て直して返す
 (interval (min (interval-big w) (add1 (guess w)))
      (interval-big w)))

(define (guess w)
 ; big-bang の環境情報の(interval 構造体)の small スロットの値
 ; と big スロットの値を利用して計算した値を返す
 (quotient (+ (interval-small w) (interval-big w)) 2))

;;; Rendering

(define (render w)
 ; big-bang の環境情報の(interval 構造体)を引数に取るのは
 ; big-bang の仕様の要求
 ; ここでは使われていない
 (overlay (text (number->string (guess w)) SIZE COLOR) MT-SC))

(define (render-last-scene w)
 ; big-bang の環境情報の(interval 構造体)を引数に取るのは
 ; big-bang の仕様の要求
 ; ここでは使われていない
 (overlay (text "End" SIZE COLOR) MT-SC))

;;; Time to Stop

(define (single? w)
 ; big-bang の環境情報の(interval 構造体)の small スロットの値
 ; と big スロットの値を利用してゲーム終了を判定する
 (= (interval-small w) (interval-big w)))

;;; テスト
(start 1 100)

構造体の場合直接「big」「small」を参照してる?

そういう事、ですね。

なるほど・・僕の場合は)がどの(に対応してるのかをすぐに把握できないという「それ」以前のレベルかな・・

いや、それは多分熟練Lisperでもそうだ(笑)。
例えば実用Common Lisp(PAIP)の著者、Peter Norvigは次のように言ってるし。

Lisp プログラムを とってきて正しくインデントし、制御構造用のカッコをとり除けば、 最終的に Python プログラムにかなり似たものができあがる。 

んで、ポール・グレアムは次のような事をANSI Common Lispと言う書籍で書いてます。

前項で定義した疑似member関数は5つの閉じ括弧で終わっていた。もっと複雑な関数の定義だと7つとか8つの閉じ括弧で終わるかもしれない。Lispを学び始めた人々は、たくさんの括弧を目にしてイライラするだろう。一体どうやってこんなコードを独りで書けって言うんだ。どうやってどの括弧が別の括弧に対応するって分かるんだ。
答えは、誰もそんな事をする必要がない、と言うものだ。Lispプログラマは括弧でコードを読み書きするんじゃなくってインデントで読んでるんだ。そして彼らがコードを書く時、テキストエディタに括弧の対応を任せる。良いテキストエディタ、特にLispシステムを含んでるもの、は括弧対応機能を持ってるだろう。その手のエディタだと、括弧をタイピングすれば対応する括弧を教えてくれるだろう。もし、貴方が選んだエディタが括弧の対応をしてなければ、一旦それ使うのを止めて、どうやれば良いのか調べよう。と言うのもそれなしでは、事実上、Lispコードを書くのは不可能だから、だ。
良いエディタさえ使えば、コードを書く際の括弧の対応、と言う問題は消えて無くなる。そしてLispのインデントは広く慣習が浸透してるんで、コードを読む場合も問題にならない。と言うのも皆同じ慣習に従っているし、コードをインデントによって読み、括弧を無視している。
しかしながら、どんなに経験を積んだLispハッカーでもこんな風に書かれたour-member関数だと読むのは難しいだろう。

(defun our-member (obj lst) (is (null lst) nil (if
(eql (car lst) obj) lst (our-member obj (cdr lst)))))

しかし、コードが正しくインデントを施されていれば、問題は生じない。殆どの括弧は無視出来るしこんな感じで読めるだろう。

defun our-member (obj lst)
 if null lst
   nil
   our-member obj (cdr lst)

事実、これが紙の上にコードを書く時の実際の方法だ。その後、これをタイピングする際、エディタの括弧対応機能の利点を活かす事になる。

両者とも実は強調してるのは「括弧を全く読んでない」と言う事。
実はインデントで読んでいる。
つまり、Lisp慣れしてないプログラマにとっては括弧は「意味が分からない」視覚ノイズにしか過ぎないんだけど、慣れれば慣れるほど、実は「括弧が見えなくなっていく」と言う事です。
だから逆に、星田さんが"("と")"の対応がすぐ分かんない、って言うのなら、むしろLispに「慣れてきている」って事かも。
もし自信がないのなら、括弧を追う、んじゃなくってインデントを追うべきです。
僕もポール・グレアムの言を読んだ後、意識的にインデントを読むようになりました。星田さんも意識的にインデントに目を向けるといいかもしんない。

もしくはエディタで自動で色分けされたら楽かもな・・とか

同じ風に考えるから、Lispやるヤツは皆Emacsに行っちゃうんだよな(笑)。



対応する括弧同士が色が同じになってるでしょ?

別にBig-bangじゃないのにこの形が出てる。

別に「連載」ってわけじゃないけど、このブログで「Racketを使ったゲーム作り」のHow-toを書くとしたら、形式は一致させた方がイイでしょ?
理論的には別に連想リスト使おうが何使おうが構わないんだけど、せっかくbig-bangで構造体を使う方針を推してたんで、形式に逆らっても読む人も混乱するだろうし・・・って事です。


これは構造体の基本をもう一度しっかりとやらんとイカン予感

ええとね、これも何度か書いてますが。
実装上の話をすると、構造体の「正体」は要するにベクタ(配列)です。
言い方変えると「要素に名前を付ける事が出来るベクタ」を構造体、と呼びます。

;;; ベクタの使用例

(define v (vector "星田オステオパシー"
       "大阪"
       "xxx-xxxx"
       "gmail"))

これでもいいんだけど、「要素」にアクセスする度に「番号」を使わないとならない。

> (vector-ref v 0) ;; 名前を知るには?
"星田オステオパシー"
> (vector-ref v 1) ;; 所在地を知るには?
"大阪"
> (vector-ref v 2) ;; 電話番号を知るには?
"xxx-xxxx"
> (vector-ref v 3) ;; 使ってるEmailを知るには?
"gmail"
>

「何らかのデータ」を取り出すには「インデックス番号を覚えてないとならない」、つまり不便だ、と言う事です。
一方、「構造体」と言う「ベクタの亜種」を使えば

> (struct Data (名前 所在地 電話番号 Eメール)) ; 構造体でユーザー定義型を作る
;;; 新しく作ったData構造体でデータを定義
> (define s (Data "星田オステオパシー" "大阪" "xxx-xxxx" "gmail"))
> (Data-名前 s) ;; 名前を知るには?
"星田オステオパシー"
> (Data-所在地 s) ;; 所在地を知るには?
"大阪"
> (Data-電話番号 s) ;; 電話番号を知るには?
"xxx-xxxx"
> (Data-Eメール s) ;; 使ってるEmailを知るには?
"gmail"
>

と「要素」(Racketではフィールド、と呼称する)に付けた「フィールド名」でアクセス出来る。
そう、名前を要素にタグ付け可能だ、ってのが構造体の便利なトコなんです。
そしてそういう「タグ付けられた要素」を、繰り返しますが、スロットとかフィールドと呼ぶ。
ちょっと纏めます。

  1. 構造体は実装上はベクタ(配列)の変種である。
  2. 構造体はRacket上でユーザーにユーザー定義型を提供する手段である。
  3. 構造体は要素を記録出来るフィールド/スロットを持ち、それらには「構造体名-フィールド名」でアクセスする事が出来る。
そして「構造体定義」はマクロで成されているので、構造体を定義、した時点で関数がいくつか「自動的に」「暗黙で」定義される。
次の2つは「確実」に定義されます。

  1. フィールドの値にアクセスする為のアクセサ
  2. 構造体定義は「新しい型」を作るので、型判定述語
上の例でのData-名前とかData-所在地と言うのは、構造体定義で「自動的に」作られた関数で、値にアクセスする目的なんでアクセサ、と呼びます。
気をつけなければならないのは、暗黙に定義されても関数だ、と言う事。
従って、構造体を定義した際に作られるこのアクセサと「同名の」関数を作ってはいけない(笑)。そうするとシャドウイングが起こってぶっ壊れます。
もう一つは構造体定義は「新しい型」を定義します。従って型判定述語も「自動的に」定義される。
上の例だと構造体名をDataにしたんでData型ってのが新しく定義されます。そして型判定述語はData?になる。

> (Data? s)
#t

そして重要なのが、実装上、構造体がベクタを利用して作られていても、もはやこれはベクタ型ではない。

> (vector? s)
#f
>

他に重要なのは、Racketの構造体で#:mutableオプションを与えると、フィールド/スロットの値を破壊的変更するミューテータと呼ばれる関数も自動で定義されます。
ただ、Racketの思想としては「データ型は不変にしたい」と言う思惑があるんで(関数型プログラミングを支援してるんで)、あまり出番は無いでしょうね(※1)。

なお、ここまで読んできて、「あれ、どっかで読んだ事あるな?」と思ったら、それは正解です。
実は以前書いた「クラス入門」と言う記事の内容と殆ど同じなんだ。あっちはPythonで書いたけど、やってるこたぁこっちで書いてる事と同じだ。
そりゃ当然で、あっちは「Pythonのclassを構造体代わりにして使え」って話だったんだから。
つまり、Racketだと、

(struct Brave (strength dexerity maxHP maxMP THAC0 AC HP MP EX name LV))

で、

(define The-Prince (Brave 5 4 28 0 0 0 0 '() 0 "えにくす" 1)) ;; ローレシアの王子

っちゅう事になる。



なお、Racketの構造体は「単一継承」も出来るスグレモノなんで、Racketにもオブジェクト指向で「クラス」が作れますが、ユーザーは圧倒的に構造体の方を使ってるでしょうね。

ところで、ちょっと触ってる感じBig-bang(2htdp?)を使ったらRacket使ってPC6001で遊んだ思い出のゲーム「ハンバーガーショップ」を実現できそうな気がして来たんで頑張ります(^o^)

おお、頑張って下さい。
なお、その「ハンバーガーショップ」と言うゲーム。BASICでのソースコードが公開されてる模様です。

※1: 構造体自体も長い間Schemeでは公式仕様に含まれてなかった。が最新仕様のR7RSでは含まれている。ただし、そちらではレコード型、と呼んでいる。
Racketの構造体は設計方針としてはANSI Common Lispの構造体、に近い。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「RE: プログラミング日記」カテゴリーもっと見る

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