FVPが急ぎます

この記事では、関数型プログラミング要素が実際にどのように役立つかについて説明します。 Habréにそのような記事がたくさんありますが、私が覚えている限りでは、このテーマに関してForthについて書いた人はいません。 さらに、私はこれについての理論を育てようとはしていません(私の理論家はまだ1人です)。 昨日遭遇した純粋に実用的なタスクについてお話します。 私の話が面白いものになることを願っています。





厳密に言えば、これはForthについてではなく、ボードゲームを記述するためにAxiom Development Kitプロジェクトの一部としてGreg Schmidtによって開発されたForthScript専用の方言についてです。 私はかなり長い間この製品を使用しており、それを使用していくつかのゲームを開発することができました。 現在、私は、 Goを連想させる、非常に興味深いゲームに取り組んでいます。 以前の記事ですでに言及しました。 次のようになります。











フィールド名の表示は特に有効になっています。 実際、開発中のZillions of Gamesプラットフォームでは、数字を「重ねて」配置することはできません。 ここの各「図」は4つの部分で構成されています-タイル。 この事実を理解することは、その後のプレゼンテーションにとって非常に重要です。 ボードは4倍大きくなり、コードはより複雑になります(ZSG表記は完全に読みにくくなります)が、結果は間違いなく価値があります。



ご想像のとおり、このゲームのボードは3次元です。 ゲームの7x7バリアントの場合、14x14x7 = 1372ポジションの状態を保存するのが最も簡単です。 ZRFではこれに問題はありません(この言語では多次元ボードを定義できます)が、残念ながらZRF自体に問題がありました。 形状除去アルゴリズムは、Margoで移動を実行する場合、この言語には複雑すぎます。 さらに、AI ZoGはおそらくこのゲームに対応しません。 Axiomを使用して、これら2羽の鳥を1石で殺そうとします。 残念ながら、Axiomは新しい課題をもたらします。 特に、この製品では3次元ボードの決定が許可されていません。



公理未定ボードの定義
7 CONSTANT DIM DIM 2 * CONSTANT COLS COLS DIM * CONSTANT ROWS {board ROWS COLS {grid} board}
      
      







ここで2次元のボードが定義されているのは簡単です。 3次元のレイヤーは、Y座標で次々に単純に「レイアウト」されますが、実際には、Axiomは次のスキームに従って1次元配列を定義します。









ボードフィールドには、左から右に上から下に番号が付けられています。 さらに、チェスボードと同様に、フィールドに名前を付けるために使用される定数が自動的に作成されます。 上記の8x7ボードの場合、位置名a2またはそれに対応する数値がコードで使用されるかどうかに違いはありません(これはしばしば便利です)。 gridを使用せずに各位置を手動で定義することは許可されていますが、14x14x7ボードのこのような説明の量、そして最も重要なことには、ロードの時間を想像することさえできません。



グリッド設計は、別の非常に便利な機能を提供します。 ゲームでは、ポジションそのものだけでなく、それらの間の関係も重要です。 グリッド構造を使用する場合、これらの関係を定義する方向は、座標の単純な増分によって指定できます。



方向の定義
 COLS CONSTANT DDIR DDIR NEGATE CONSTANT UDIR {directions -1 0 {direction} n 1 0 {direction} s 0 1 {direction} e 0 -1 {direction} w -1 -1 {direction} nw 1 -1 {direction} sw -1 1 {direction} ne 1 1 {direction} se DDIR 0 {direction} down UDIR 0 {direction} up directions}
      
      







従来、方向の名前には、基本ポイントの名前に応じて1文字と2文字の指定が使用されます。 方向を定義すると、次のコードを簡単に記述できます(図の北への移動を制御します)。



 : move-to-north ( -- ) n verify (   ,     ) empty? verify (    ) from here move (   ) add-move (    ) ;
      
      





もちろん、そのような(非常に小さな)関数を各方向に1つ記述するのは非常に面倒です。 ここで初めて(ただし最後ではない)、 高次関数が助けになります。 方向nは、現在の位置を(副作用として)変更し、成功の兆候を返す関数です。 この関数のアドレス(Forthの用語では単語)を取得して、別の関数に渡すことができます。 必要なのは、受信したアドレスでホストされている機能を実行する能力だけです。 方法は次のとおりです。



スタックに関数を渡す
 : move-to ( 'dir -- ) EXECUTE verify (     ,     ) empty? verify (    ) from here move (   ) add-move (    ) ; : move-to-n ( -- ) ['] n move-to ; : move-to-s ( -- ) ['] s move-to ; : move-to-w ( -- ) ['] w move-to ; : move-to-e ( -- ) ['] e move-to ;
      
      







これは非常に一般的なAxiomのイディオムであり、まれなプログラムでこれを使用することもできます(もちろん、このバランスをとる行為すべての感覚は、一般化された機能がより複雑です)。 しかし、マーゴに戻ります。 上記で説明したボードの実装は、8x8のサイズ(16x16x8 = 2048ポジション)まで機能しますが、9x9ボード(18x18x9 = 2916ポジション)では既に機能しなくなります。 どうやらこの値は最大許容ボードサイズよりも大きいようです(Axiomのドキュメントでこの制限についての言及は見つかりませんでした)。 もちろん、私はこれを受け入れることができませんでした。 私このボードで長くて苦労しを描いだけでなく(実際、4人のサンサンと1人の天元が 、このサイズのボードに必要なものです)。



記事の冒頭の図と、上記のボードの定義をよく見ると、主に空気が蓄えられていることがわかります。 2番目以降の各レイヤーには、ますます少ないシェイプが含まれます。 数字が表示されない行を保存することにより、RAMを非常に合理的な方法で消費しています。 すべてがこのように機能する場合、最適化しても意味がありませんが、Axiomの制限にぶつかったので、後続の各レイヤーの行数を少なくしてみませんか? これも最適な方法ではありませんが、前の方法よりもはるかに経済的です。



最適化されたボード定義
 9 CONSTANT DIM DIM 2 * CONSTANT COLS 90 CONSTANT ROWS COLS 1- CONSTANT DDIR DDIR NEGATE CONSTANT UDIR {board ROWS COLS {grid} board}
      
      







9x9ボードの場合、保存する必要があるのは18x90 = 1710ポジションのみです。 その量の公理は非常に有能です。 ボードの「深い」方向を決定するために使用されるDDIR定数の変更に注意してください(ボードを上下逆さまに保管することを忘れていました)。 残念ながら、必要な変更はこれだけではありません。 ゼロライン上の任意の位置から下に移動しようとするとどうなりますか? DDIRが1つ小さくなったため、同じレイヤーの最後の行に到達します! これは、ゲームのロジック全体を破壊する可能性があります。



また、レイヤーの最後の行から南へ、または最初から北へ行くと、最初のレイヤーのゼロラインを除いて、ひどく判明する可能性があります。 不要な接続をすべて手動で禁止することもできますが、私は自分の手で多くの作業をするのはあまり好きではありません。 インテリジェントな方法で問題を解決してみましょう。 まず、現在の座標を決定する方法を学びます。 それは非常に簡単です:



 : get-x ( pos -- x ) COLS MOD ; : get-y ( pos -- y ) COLS / ;
      
      





レイヤーの境界線の定義はもう少し複雑です:



 : is-edge? ( -- ? ) COLS here get-y BEGIN 2DUP <= IF OVER - SWAP 2 - SWAP FALSE ELSE TRUE ENDIF UNTIL SWAP 1- OVER = SWAP 0= OR ;
      
      





このスタックアクロバットの本質は単純です。 現在の行番号から、レイヤー幅を減算します(そこから減算するものがあるまで) 。COLSで始まり、ループの各反復でこの値を2ずつ減らします。 その結果、値がリセットされるか、現在のレイヤー幅よりも1つ小さくなると、境界線になります。 これで、これらの行からのすべての移動を簡単に禁止できます(目的の方向に向けて)。



 {directions -1 0 {direction} n-internal 1 0 {direction} s-internal 0 1 {direction} e 0 -1 {direction} w -1 -1 {direction} nw 1 -1 {direction} sw -1 1 {direction} ne 1 1 {direction} se DDIR 0 {direction} d-internal UDIR 0 {direction} u directions} : common-dir ( 'dir -- ? ) is-edge? IF (  ? ) DROP FALSE (         ) ELSE EXECUTE (         ) ENDIF ; : n ( -- ) ['] n-internal common-dir ; : s ( -- ) ['] s-internal common-dir ; : d ( -- ) ['] d-internal common-dir ;
      
      





再び高次関数を使用してください! 残念ながら、このコードには重大なエラーが含まれています。 実際には、下方向のみ、境界線からの移動を禁止する必要があります。 北方向は上部の境界線でのみ禁止され、南は下部で禁止されます。 エッジ述語 選択した方向に応じて、異なる方法で計算する必要があります! 私たちの前に再び「コピー&ペースト」の不吉な影が現れます。 幸いなことに、いくつかの引数から関数へのポインタを渡すことを禁止する人はいません。 正しい実装は次のとおりです。



最終的な解決策
 : is-edge? ( 'op -- ? ) COLS here get-y BEGIN 2DUP <= IF OVER - SWAP 2 - SWAP FALSE ELSE TRUE ENDIF UNTIL SWAP 1- OVER = SWAP 0= ROT EXECUTE ; : my-first ( ab -- b ) SWAP DROP ; : n ( -- ) ['] n-internal ['] my-first common-dir ; : s ( -- ) ['] s-internal ['] DROP common-dir ; : d ( -- ) ['] d-internal ['] OR common-dir ;
      
      







目と心を開いておいてください。 関数型プログラミングを実行するために、学術活動に従事する必要はありません。 高次関数は、思っているよりも近いかもしれません。




All Articles