見出し画像

Retro-gaming and so on

RE: Racketで何かゲームらしきものをつくれ!008 ショップ関数完了&バトル関数書き直し1

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

ホント楽しそう。
そうなんだよな、プログラミング上達する一番のコツ、ってのは「自分で何か作ってみる」って事だ。
楽しくなんか作るのが一番、であって、この辺、アカデミックな「プログラミング講座」では決して得られない部分だ。

結局、アカデミックなプログラミング講座、ってのは学生に「技術的な引き出し」を作らせる事に基本終始してるわけだが、そのテの「技術の索引」を作っても、学生自身の「何か作りたい」ってモティベーションとは必ずしも直結せんのだよな。
言い換えると、アカデミックな授業では、「いつか困った時の為のテクニック」を教えてるわけなんだけど、その「いつ困るのか」ってのは困ってみなけりゃ分からない、と言う(笑)。

いずれにせよ楽しくしてるんで上達が早い。ガンガン自分が「望む」機構を組み上げて行ってて関数が複雑になっていっても躊躇してない。
これはマジですごい事だ。

もう一つ、星田さんが優れてるのは。


自分が書いた関数をコメントアウトして保存しているトコだ。
そう、人が提示した関数をそのまま受け入れちゃダメなんだよ。星田さんはその辺良く分かってる。
プログラミングを勉強してる人はこの辺、星田さんのやり方から学んだ方が良いと思う。
自分が書いたコードが間違ってようと正解だろうと、「書いたモノを保存しておく」ってのは重要だ。
何故なら自分自身のアビリティアップの進捗具合が良く見えるから、なんだよな。
つまり、「元来自分が何をやりたくて、どこで混乱したのか」良く見えるようになる。
これ、簡単に出来るようで、なかなか出来ないんだ。

さて、今回のテーマは述語の作り方だ。

「でも述語って真偽値を返すだけでしょ?」

って思うかもしんない。その通りだ。
一応前提を言っておくと、ここから書く事は別にLisp限定のテクニックではない。どの言語でも基本通用する普遍的なモノだ。
ただし、色々見てみると、なんかLispプログラマだけが理解してるように見えるんだよな。他の言語でも通用する筈の話なんだけど、どういうわけかコーディングスタイルに落とし込まれてるのはレアだ。ひょっとしたら構文スタイルのせいかもしれない。
いずれにせよ、星田さんが今後別のプログラミング言語に移動する事も考えて、ユニバーサルな述語の作り方を書いてみる。


これは第一段階のモックアップとしては全然O.K.です。再帰関数としても非常に良く書けてる。
ただし、Lispプログラマはそこで終わらない
ちょっと構造を見てみよう。



つまり、条件節で真偽値判定して真あるいは偽が返ってきてるのに、わざわざ返り値として自分で真偽値を書き込んでいる。
Lispプログラマはこれを無駄だと考えるんだ。言っちゃえば「ダサくね?」って思うんだよ。
もう一度言うけど、Lispプログラマはモックアップでこうは書く。ただし、「上手く動く」とロジックを確認さえしたら速攻リライトを始めるだろう。
とにかくLispプログラマはコード上の「面倒臭さ」を嫌うんだ。あらゆる手を尽くして無駄を排除、そしてコードの短縮化に動くだろう。言ってしまえば若干病的なんだけど(笑・※1)。
ここで使うのが論理演算だ。andornotを駆使する。
マクロandは含まれる引数が全部 #t だったときに #t を返す。言い換えれば1つでも #f を見つけたらその時点で #f を返しちまうんだ。
つまり、コードの件の部分の冒頭、

(if (null? Cequip-list) ;; ここの条件節は#t/#fを返す
 #f            ;; でも返り値は #f

では、要するに「Ceqquip-listが空リストじゃなかったら計算を継続、そうじゃなかったら#fを返せ」って事なんで、

(and (not (null? Cequip-list)) ...

かあるいは、

(and ((compose not null?) Cequip-list) ...

として書き始めればいい。
いずれにせよ、(not (null? Cequip-list))((compose not null?) Cequip-list)#fを返した時点で、andは速攻評価を止め、#fを返して計算を終了する。そしてそれが望んでた動作だろ?
次の節を見てみよう。

(if (string=? index (caar Cequip-list)) ;; ここの条件節は#t/#fを返す
 #t ;; でも返り値は #t
 (equip? (cdr equip) index)) ;; ここで再帰

文字通り、「index(caar Cequip-list)が等しければ #t を返せ、じゃなければ再帰しろ」との事なんだけど、言い換えると節のどっちかが#tだったらそれでいい、って意味になる。
この時に使うマクロがorだ。orは含まれる引数の「どれか」が#tだった時にそこで評価を止めて#tを返す。そしてぶっちゃけ、引数の中身が再帰関数でもちっとも構わない。
従って、この部分は、

(or (string=? index (caar Cequip-list)) (equip? (cdr equip) index))

って書いちまって構わない。
つまり、全体では、



と書くって事だ。返り値としての記述である #t#f は全部消えてしまった。
一見「論理演算」って言えば数学的に聞こえるけど、Lispプログラミング上は全く関係がない。述語が#tあるいは#fを返すのがわかりきってるのに、「自分で返り値としてそれを書くのは無駄だし無粋だ」と考えるのがLispプログラマなんだ。言語が出来る事は言語に任せて、そこには立ち入らないのがスマートだ、と連中は考える。言わばどっちかっつーと丸投げ精神だ(笑)。ここでも正しく丸投げするか否か、が問われるわけ。

例えばLispのzero?なんかも、

(define (zero? arg)
 (if (= arg 0)
  #t
  #f))

とは書かない。次のように書く。

(define (zero? arg)
  (= arg 0))

真偽値を返す、って分かりきってるならその返り値をそのまま使うようにしよう。言っちゃえばそれが「論理演算」なわけ。
一方、同様のテーゼを与えられると、圧倒的に、例えばPythonなんかでは、こう書く人が多いんだよな。

def is_zero(arg):
 if arg == 0:
  return True
 else:
  return False

実はこう書いて良い、って事に気づいてない人が多い。

def is_zero(arg):
 return arg == 0

だから、Lispプログラマ以外は気づいてないんじゃねぇの、と思うわけ。でもこれは基本的にはユニバーサルなテクニックだ。
与題の関数なんかでもPythonでは次のようにして書くのが本来だったら望ましい。

def is_equipped(equip, index):
 Cequip_list = [x for x in equip if x[1] != 0]
 return Cequip_list != [] and (index == Cequip_list[0][0] or is_equiped(equip[1:], index))

もっとも、Pythonは再帰が苦手なんで、こういう関数を書けば実装上は困った事になるケースが多いだろうけどね(笑)。Pythonは再帰に対して脆弱だから。
ま、いずれにせよ、Pythonの再帰はさておき、「考え方」としてはどこでも実は使えるモノなんだけど、繰り返すけど、意外にLispプログラマ以外はなかなか気づかないテクニックだ。
よってLispをやってるウチにマスターしておこう(※2)。

次の話題も真偽値にまつわる話だ。


これは多分動かないだろう。
問題はここ、だ。

(if (= #f (enemy-human (car enemies)))
 ...

=は数値用の判定述語なんで、真偽値の比較は出来ない。
やりたい事は恐らく、notを使った条件節だ。

(if (not (enemy-human (car enemies)))
 ...

注: だいぶ端折って書いたが、Schemeの仕様上、andorは、真と判定されたモノを返す条件自体は違うが、実はその引数の評価値を返すように設計されていて、別に#tと言うシンボルを返すようには設計されていない。
しかし、Scheme系言語に於いては、#fじゃなければ全部真なので、論理的には破綻していない。そしてLisp系言語に良く見られる使い回せるブツだったら使いまわそうの精神で、得るのが可能な情報を#tで塗りつぶしたりしないわけだ。
今回見たequip?#t#fしか返さない述語として成り立ってるのは、要するにandないしorに与えられた引数が全部述語で、言っちゃえば、それらの「評価結果」を利用して#tを返すようになってる、って事だ。つまり、引数の述語の判定結果を文字通り使い回ししている。
また、orがどうして再帰関数を受け入れるのか、と言うのも同様の理由で、原理的には、第一引数が#fである以上、再帰関数が#tを返す事を期待せなアカンわけだ。実際は#fを返す事もあるわけだが、いずれにせよ第一引数が#fな以上、最後の引数がどうなってもそこが返す値を返さなきゃならないわけ。
なお、実質的にはequip?は末尾再帰関数になってて、それはorの第一引数が #t だったら第二引数の再帰部分は全く評価されない。つまり、関数と違って全引数を評価してからそれがorに渡されるのではなく、orは与えられた引数を評価する前にifで書かれたS式にマクロ展開される。
そうなれば、orに渡された最後の引数はこのケースの場合、明らかに末尾再帰の末尾呼び出し部分としてifが構成するS式のネストの最後に記述される事となるわけだ。

※1: ただし、ANSI Common Lispのユーザーはここで書かれてる程病的にしつこい論理演算は行わないだろう。それはANSI Common LispとSchemeの重要な機能の差に拠る。
何度も書くが、ANSI Common Lispの関数もマクロも必ず値を返す。従って、ANSI Common Lispのwhenunlessも値を返す辺りがSchemeのそれらと違う。
従って、ANSI Common Lispのユーザーは、andornotを駆使するよりは、ある程度whenunlessを使ったコードの見た目の「良さ」の方を重要視する傾向がある。
結果、与題のコードはANSI Common Lispだと、


と書く方が好まれるかもしれない。

※2: 結局、この書き方がフツーの言語で通常行われないのは、やはり「力が弱い」からだと思う。
星田さんは非常に簡易に再帰関数を書いてたが、実際問題、再帰関数をキチンと扱える言語は数少ない。
結果、論理式に再帰を突っ込んだ場合、負荷的なモノをLisp以外の言語は避けようがないケースが多いわけだ。
ここで見たPythonの例でも、基本的にはwhileで書いた方が効率は良く、結果、


としか書き様がない。そして条件判定と返り値が一致しようと、returnしない以上ループから脱出出来ないんで、結果、Lisp的な観点で言うと「無駄に思える」事柄を再び改めて返り値として書かざるを得なくなるわけだ。
言語がパワフルであれば、無駄が省けるんだが、そうじゃない場合は、Lispプログラマ的には「苦渋の決断」をせなアカン、と言う良い例だ。
力の弱い言語を使う以上、本質的な「コードの冗長さ」を我慢せなアカンくなるわけだ。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

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

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