ステロイドのZoG

ゲーム「 Thud! 」の開発について書いたとき、受け取った記述の冗長性についてすでに文句を言いました。 ZRF言語のシンプルさには裏返しがあります。複雑でないものを記述するために、多くの場合、重要なコードフラグメントを複製する必要があります。 ご存知のように、このような冗長性は、手作業の量の増加につながるだけでなく、コード内のさまざまなエラーのリスクを大幅に増加させます(ZoGアプリケーションのデバッグプロセスはささいなことではないため、これは重要なポイントです)。



このような冗長性にどのように対処できますか?



もちろん、マクロを使って! 問題は、ZRFマクロはこのために十分な表現力がないことです。 SciroccoTyphoonのゲームを開発する過程で、Adrian Kingは同様の結論に達し、外部プリプロセッサとして動作する独自の拡張マクロ言語を開発しました。 今日は、この言語の可能性について話し、Thud!の例を使用して、ZRFアプリケーションの開発プロセスでの使用を示してみます。





上で述べたように、ソースファイル(拡張子は.prezrf)を通常のzrfファイルに変換する外部プリプロセッサについて話しています。 プリプロセッサ自体はJava言語で開発されており、jarファイルです。 prezrfファイルを処理するには、次のコマンドを実行します(Javaがコンピューターにインストールされている場合)。



java -jar prezrf.jar MyFile.prezrf
      
      





処理がエラーなしで成功した場合、結果のzrfファイルは同じディレクトリに生成されます。



新しい言語は私たちにどんな機会を提供しますか? 最初に、彼は新しいタイプのマクロを紹介します。 prezrfマクロを定義するには、 defineキーワードを使用します! (感嘆符がすべての新しいキーワードに最後に追加されました)。 元のZRF 定義マクロはプリプロセッサによって無視され、単に出力にコピーされます。 新しいサンプルのマクロの定義は、 undefineでキャンセルできます 。 この機能は、新しいマクロを他のマクロでローカルに定義できるため便利です(ZRFはこれを許可していません)。



キーワード展開! 指定されたコードの場所で以前に定義されたマクロの「展開」につながります。 このアクションは非常に頻繁に実行されるため、略語が定義されています! '。 したがって、次のコードを処理することにより:



 (define! swap ($2 $1) ) (! swap ab)
      
      





...出力が得られます。



 (ba)
      
      





このマクロは、コマンド(感嘆符)によって感嘆符なしで展開されますが、 展開を使用します! 誤字を防ぐことができます。 たとえば、何らかの理由で、スワップの定義で定義するために感嘆符を追加するのを忘れた場合、構築(swap ab)が出力に単純に複製され、マクロが拡張構築で呼び出されます! 、エラーの形成につながります: expand!:undefined macro“ swap”



prezrfが提供する新機能がなければ、これらすべては、おそらく非常に興味深いものではないでしょう。 新しいタイプのマクロでは、リスト引数を使用できます! さらに、便宜上、リストとしてマクロに渡されるいくつかの引数にリンクする機能が追加されました。 $ 2 * 4構造は、マクロに渡された2番目、3番目、および4番目の引数の値を順番に出力します(少なくとも4つの引数が渡された場合)。 また、明らかなセマンティクスを持つ略記法$ n *および$ * mが定義されています。 この機会を利用して、たとえば、マクロに渡された引数の数を数えることができます。



 (define! count (length! ($1*)) ) (! count abc) ; => 3
      
      





この例では、 $ 1 *を囲む括弧が必須であることに注意してください。最初のマクロから開始して、マクロのすべての引数の値をリストするリストを作成します。 括弧がないと、 長さがあるため処理エラーになります! 1つのリスト引数のみを受け入れます。 ただし、このマクロは入力エラーから十分に保護されていません。 引数なしの呼び出し(カウント)は失敗します。 これは次のように修正できます。



 (define! count ($?1 (length! ($1*)) ) ($!1 0) )
      
      





ここで、マクロに1つ以上の引数が渡された場合は$?1が実行され、そうでない場合は$!1が実行されます。 さらに、 $ -n構造を使用して、リストの末尾から要素に番号を付けることができます。 これらの機能はすべて、将来的に非常に役立ちます。



自尊心のあるプログラミング言語と同様に、prezrfは条件付き実行( if-less!、 If-less-or-equal! )およびループ( for! )構造を提供します。 if-constructions(および上記の言語にはわずかに多くあります)は、同様のZRFコンストラクトとは異なり、 elseブランチを定義しないでください リスト項目を走査するためにのみ使用できます。 たとえば、ゲームで定義されているすべての方向に対してアクションの実行を繰り返すことができます(これは非常に頻繁に必要です)。



 (define! -all-directions (n ne e se s sw w nw)) (define! shift-all (for! $d ($ -all-directions) (shift $d) ) ) (! shift-all)
      
      





このコードでは、制御構造 ' $ 'が使用されていますが、これについてはまだ説明する時間がありません。 彼女は何をしているの? 実際、これは非常に一般的に使用される設計の略記です:



 (!! (! macro))
      
      





拡大! ここで私たちはすでにおなじみですが、何をしますか!! '? このコマンド( expand-first! )は、親コンストラクトの要素自体ではなく、要素の値を使用するようにプリプロセッサに指示します(この例ではfor! )。 この点はあまり明確ではないかもしれませんが、言語を理解するために非常に重要です。 (!-All-directions)を使用した場合、出力は次のようになります。



 (shift !) (shift -all-directions)
      
      





これは明らかに私たちが望んでいたものではありません。 ごめんなさい リストでのみ機能し、次の不名誉を最適化することはできません:



だからトロルに行く
 ( define troll-1 ( $1 (verify empty?) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-2 ( mark (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-3 ( mark (opposite $1) (verify friend?) (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-4 ( mark (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-5 ( mark (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-6 ( mark (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) ) ( define troll-7 ( mark (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) (opposite $1) (verify friend?) back $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) $1 (verify empty?) (verify (or (enemy? n) (enemy? nw) (enemy? s) (enemy? ne) (enemy? w) (enemy? sw) (enemy? e) (enemy? se))) (if (enemy? n) (capture n)) (if (enemy? nw) (capture nw)) (if (enemy? s) (capture s)) (if (enemy? ne) (capture ne)) (if (enemy? w) (capture w)) (if (enemy? sw) (capture sw)) (if (enemy? e) (capture e)) (if (enemy? se) (capture se)) add ) )
      
      







しかし、ループが通過する場所では、再帰が助けになります。



言ったように、再帰
 (define! repeat (if-less! 0 $1 $2* (! repeat (!! (sum! $1 -1)) $2*) ) ) (define! troll-n (if-less! 0 $1 (for! $d ($ -all-directions) ( (if-less! 1 $1 mark (repeat $1 (opposite $d) (verify friend?) ) back ) (repeat $1 $d (verify empty?) ) (if-less! 1 $1 (verify (or (for! $dd ($ -all-directions) (enemy? $dd) ) ) ) ) (for! $dd ($ -all-directions) (if (enemy? $dd) (capture $dd) ) ) add ) ) (! troll-n (!! (sum! $1 -1))) ) ) ... (! troll-n 7)
      
      







これは、元のバージョンよりも理解するのが多少困難ですが、コピーアンドペーストはできません。 repeatの実装に特に注意してください。 このマクロは非常に便利であり、今後も繰り返し使用されます。



 (! repeat 3 abc) ; => abcabcabc
      
      





Thud!の元の実装では! 最適化する場所がもう1つあります。



 ( define check-rock ( check-rock-direction n ne e se s sw w nw) ( check-rock-direction ne e se s sw w nw n) ( check-rock-direction e se s sw w nw n ne) ( check-rock-direction se s sw w nw n ne e) ( check-rock-direction s sw w nw n ne e se) ( check-rock-direction sw w nw n ne e se s) ( check-rock-direction w nw n ne e se s sw) ( check-rock-direction nw n ne e se s sw w) )
      
      





8つの要素のセットのすべての循環順列を手動で列挙することにより、ミスを犯しやすくなります。 まず、リストの一部を「切り取る」ことができるマクロを定義します。



 (define! range (if-less-or-equal! $1 $2 (nth! $1 $3) (! range (!! (sum! $1 1)) $2*) ) ) (! range 3 4 (abcde)) ; => cd
      
      





ここで、関数(nth!N list)を使用すると、リストのn番目の要素を取得できます。 リストを「回転」させてみましょう。



 (define! rotate (if-less! 0 $1 $2 (! rotate (!! (sum! $1 -1)) ((splice! ((! range 2 8 $2)) ((nth! 1 $2))))) ) ) (! rotate 2 (abcdefgh)) ; => (abcdefgh) (bcdefgha)
      
      





正常なようですが、すでに(!Rotate 3(abcdefgh))エラーが発生します:



エラーメッセージ
[list "(nth!2((splice!((!Range 2 8(abc" ... [list "(nth!$ 1 $ 3)" at t.prezrf、line 3] for [list "(range 2 8((splice!((!Range 2 8(ab "... at expand!Of [list"(!Range 2 8((splice!((!Range 2 8(a "... at argument substitution in [list"( !range 2 8 $ 2) "at t.prezrf、line 11] for [list"(rotate 2((splice!((!range 2 8(ab "... at expand!of [list"(!rotate 2((splice !((!range 2 8(a "... at argument substitution in [list"(!rotate(!!!(sum!$ 1 -1))))((splice!( "... at t.prezrf、line 11] for [ [list "(!rotate 3(abcdefgh))" "t.prezrfの15行目]]]]]]]のexpand!のリスト「(rotate 3(abcdefgh))」:

[list "(nth!2((splice!((!Range 2 8(abc" ... [list "(nth!$ 1 $ 3)" at t.prezrf、line 3] for [list "(range 2 8((splice!((!Range 2 8(ab "... at expand!Of [list"(!Range 2 8((splice!((!Range 2 8(a "... at argument substitution in [list"( !range 2 8 $ 2) "at t.prezrf、line 11] for [list"(rotate 2((splice!((!range 2 8(ab "... at expand!of [list"(!rotate 2((splice !((!range 2 8(a "... at argument substitution in [list"(!rotate(!!!(sum!$ 1 -1))))((splice!( "... at t.prezrf、line 11] for [ [list "(!rotate 3(abcdefgh))" "t.prezrfの15行目]]]]]]]のexpand!のリスト「(rotate 3(abcdefgh))」:

1項目のリストの項目#2を取得しようとしています



ご覧のとおり、C ++テンプレートのコンパイル時のエラーメッセージほど広範囲ではありませんが、それほど明確ではありません。 かなり前からprezrfをいじり、比較的簡単に作業できるという2つの経験則を思いつきました。



  1. 可変リストを引数として再帰マクロに渡さないでください
  2. forの使用を制限してください! 最も単純なケース


これらのルールからの逸脱は、時々、私の心を吹き飛ばす恐れがあり、何が起こったかの本質を理解しようとする無駄な試みです。 最初のルールの精神でローテーションを言い換えてみましょう。



 (define! rotate (if-less! 0 $1 ((splice! ((! range (!! (sum! $1 1)) (!! (length! $2)) $2)) ((! range 1 $1 $2)) )) (! rotate (!! (sum! $1 -1)) $2) ) (if-equal! 0 $1 $2 ) )
      
      





彼は怖く見え始めましたが、彼は働いています! 彼と今どうする? 理由には順列が必要です。これらの引数をcheck-rock-directionに渡す必要があります。 もちろん、マクロを呼び出すことで対応する変更を行って回転させることもできますが、これにより回転が普遍的ではなくなります。 どうやら、関数型プログラミングの秘密兵器- 高階関数を明らかにする時が来たようです



 (define! map ($!3 (! map $1 $2 (!! (length! $2))) ) ($?3 (if-less! 0 $3 ($1 (!! (nth! $3 $2))) (! map $1 $2 (!! (sum! $3 -1))) ) ) ) (define check-rock (! map check-rock-direction (!! ((! rotate (!! (sum! (!! (length! ($ -all-directions))))) ($ -all-directions) ))) ) )
      
      





私はそれをいじらなければなりませんでしたが、それは価値がありました。 このすべてのコードの主な場所は次のとおりです: ($ 1 ...) それも機能します。これは朗報です。 実際、結果のZRFファイルのサイズに対応していない場合は、 このオプションを停止できます。 14キロバイトのソースからすべてのマクロを展開すると、プリプロセッサは1メガバイト以上の記述ファイルを受け取ります! 言うまでもなく、ZoGはこのZRF-kuをかなりゆっくりダウンロードします。



この不幸を克服する方法は? はい、マクロの助けを借りて(これ以上何もありません):



マクロはマクロを作成します
 (define! troll-n (if-less! 0 $1 (define (concat! troll - $1) ( (if-less! 1 $1 mark (repeat (!! (sum! $1 -1)) (opposite (concat! $ 1)) (verify friend?) ) back ) (repeat $1 (concat! $ 1) (verify empty?) ) (if-less! 1 $1 (verify (or (for! $dd ($ -all-directions) (enemy? $dd) ) ) ) ) (for! $dd ($ -all-directions) (if (enemy? $dd) (capture $dd) ) ) add ) ) (! troll-n (!! (sum! $1 -1))) ) ) (! troll-n 7) (define! troll-all (if-less! 0 $1 (for! $d ($ -all-directions) ( (concat! troll - $1) $d ) ) (! troll-all (!! (sum! $1 -1))) ) )
      
      







構造(!Troll-n 7)は、ZRFマクロtroll-1、troll-2、... troll-7の定義を順番に作成します(シーケンスの順序は重要ではありません)が、 (!Troll-all)は適切な場所で呼び出されます彼らの課題をリストします。 デザインに注意を払う価値があります(concat!$ 1) 。 このように複雑な方法で、ZRFマクロの本体に$ 1を形成します。 $ 1とだけ言うと、うまくいきません。



結果が遅くなることはありません。 結果のZRFファイルのサイズは28キロバイトに削減されます。 さらに小さくすることもできますが、これにはあまり意味がありませんでした。 それはオリジナルと同じように機能します。つまり、長い旅で間違いを犯しなかったことを意味します。



説明したプリプロセッサは完全に無料で使用でき、クロスプラットフォームです。 インストールされたJavaのすべての幸せな所有者は、それを試すことができます。 もちろん、彼の労苦の結果はZoGデモ版では発表できませんが、ここで異常なプログラミングを行っていますか?



ご注意
記事の例として、有名な芸術家モーリス・エッシャーの作品が使用されました






All Articles