4

問題

PureScript でゲーム エンジンを作成しようとしています。私はそれに慣れていませんが、以前に実世界の Haskell を経験したことがあるので、習得はスムーズでした (ただし、Haskell を「実際の」ものに使用した経験はあまりありません)。可能な限り多くの実行時エラーをコンパイル時エラーに変換することは、私の本では勝利ですが、言語が問題を抽象化する私の能力を過度に制限していることが判明した場合、その勝利の一部を取り除くことができます。

さて、私は HTML5 Canvas/context2d を介して PureScript で 2D ゲーム エンジンを構築しようとしています (明らかに、purescript-canvas はこれに最適な選択です - Elm の Graphics.Canvas モジュールよりもはるかに好きです。実際の基礎となる JS API により近くなり、特にキャンバスの個々のピクセルにアクセスできるようになります)。

私の既存の (未完成だが使用可能な) JS エンジンのコア機能は、「スプライト」のリストを保持し (それらはすべて共通のクラスを共有することを除いて異種)、それらすべてをループしてメソッドを呼び出す.update(timeDelta)こと.draw(context2d)でした。

スプライトはすべて共通のインターフェースを共有していますが、内部で根本的に異なるデータをサポートする必要があります。x/y 座標があるかもしれません。別の (おそらく環境効果を表す) には、「完了率」またはその他のアニメーション状態がある場合があります。

問題は、FFIを悪用して非常に不純なコードに侵入することなく、必要なことを行う同等の抽象化(異種/共有クラスリストへの)を思いつくことができないということです。

ソリューション (およびその問題)

異種リスト (duh)

明らかに、異種リストと同等のことを行うことができる最善の抽象化は、異種リストです。

Haskell スタイル

Haskell (つまり、公式の仕様/レポートではなく、だまされた GHC) がまさに私が望むものを提供していることがわかりました。クラスの制約を維持しながら、型情報をボックス化して、リスト内のすべての項目に単一のポリモーフィック関数を適用できます。型安全性を壊すことなく。これは理想的ですが、残念ながら PureScript では現在、次のような型を表現することはできません。

data ShowBox = forall s. Show s => SB s

PureScript スタイル (最先端)

PureScript の場合、purescript-existsパッケージがあります。これはおそらく、すぐ上の Haskell ソリューションと同等の機能を提供することを目的としており、型情報を非表示にするのではなく、削除して、元に戻すことができます。これにより、異種のリストを作成できますが、型の安全性が完全に失われます。

もっと言えば、私はそれを満足のいくように機能させることができないと思います[Exists f].forall a. (Draw a) => aタイプ私は元に戻します。抽出する必要がある「実際の」種類のタイプを示す何らかの「タグ」を含めることもできますが、そのような種類の悪ふざけを引っ張っている場合は、プレーンな JS でコーディングすることもできます。私がしなければならないことがあります(リストについては、スプライトが含まれているとは限りません)。

すべての状態が 1 つの大量のデータ値に

個々のスプライトのすべての状態を 1 つの大規模な構造で表し、それを各スプライトの「更新」実装に渡すことにより、すべてのスプライトを同じタイプを持つものとして統合できます (まだクラス ポリモーフィズムを使用できませんが、突然変異を含めることができます)。タイプの一部として個々のスプライト値の関数を作成し、それを使用します)。これには明らかな理由があります。各スプライトには、他のスプライトのデータを変更/更新する自由があります。大規模なデータ構造は、表現する必要がある新しい種類のスプライトの状態ごとにグローバルに更新する必要があります。エンジンを使用するすべての人がエンジンを変更する必要があるため、ライブラリを作成できません。JSでも構いません。

分離均質型

または、各スプライトが個別の状態を持ち、すべてが同じ状態表現を持つこともできます。これにより、「お互いのパイに指が入る」というシナリオは回避できますが、すべてのスプライトのニーズに関する過剰な知識、不要なタイプ構造のビットの大量の無駄なデータで更新する必要がある均一な構造がまだあります。すべてのスプライトによって。非常に貧弱な抽象化。

JSON でさまざまなデータを表すか、何を持っているか

ええ。この方法は、基本的にJS データを使用し、PureScript のふりをしているだけですPureScript のタイピングのすべての利点を捨てなければなりません。

抽象化なし

それらすべてをまったく無関係なタイプとして扱うことができました。これは、新しいスプライトを追加したい場合は、最も外側の関数を更新して を追加する必要があることを意味しdrawます。おそらく考えられるすべての解決策の中で最悪です。drawThisParticularSpriteupdate

私がおそらくすること

利用可能な抽象化の選択肢の評価が正しいと仮定すると、必要なことを行うにはFFIを何らかの方法で悪用する必要があることは明らかです。おそらく、次のような統一されたレコードタイプがあります

type Sprite = { data: Data, draw: Data -> DrawEffect, update: Data -> Data }

DataたぶんExists fある種のような、型抜きされた厄介なものはどこにありますか?

type DrawEffect = forall e. Eff (canvas :: Canvas | e) Context2D

か何か。drawメソッドとメソッドはどちらも個々のupdateレコードに固有であり、どちらも から抽出する真のタイプを「認識」していますData

それまでの間、PureScript の開発者たちに Haskell スタイルの実存的なものをサポートする可能性について尋ねて、型の安全性を壊すことなく適切で真の異種リストを取得できるようにしたいと思います。主なビットは、(以前にリンクされたHaskellの例の場合)、ShowBox(非表示の)メンバーのインスタンス情報を保存する必要があるため、関数Showの独自のオーバーライドから、使用する正しいインスタンスを知ることになると思いますshow.

嘆願

PureScript で現在利用可能なオプションに関して、上記が正確かどうか誰か確認してもらえますか? 特に、この問題に対処するためのより良い方法を見つけた場合、特に抽象化を犠牲にすることなく「純粋な」コードのみを使用できるようにする方法がある場合は、お知らせください。

4

2 に答える 2

2

@ phil-freeman(および他の読者):参考までに、回答のExists部分から適応したコードの完全な動作バージョンをここに示し、自分で確認します(下部にあります)。(これは、コメントのテキスト長の制限を回避するための自己回答であり、実際の回答だからではありません)

したがって、Exists の機能のいくつかの重要な側面について、私が誤解していたことは明らかです。ソースコードは読んでいましたが、PureScript は初めてなので、ランク 2 のrunExists. Rank-N 型については聞いたことがあり、それらが a の範囲を制限していることは理解していましたforallが、なぜそれが有用なのか理解できませんでした。:)

私が理解しているように、 for を使用するとrunExists、関数の引数がDrawOps一部だけでなくすべてに適用されるようになりDrawOpsます。そのため、自己認識と DTRT と update メソッドに依存する必要があるのはそのためです。

非例で何をしていたのかを理解するのにも少し時間がかかりましたが、今は理解できExistsたと思います。おそらく、Haskell の遅延評価ではそのような手法は必要ないと思われるためですが、PureScript ではもちろん、一度にすべてを展開することを防ぐ関数である必要が\_ -> ...ありDrawableます。updated

Exists非メソッドは、それ自体以外のデータを操作することを許可しないため、おそらく劣っていると考えていました...しかし、もちろんリフレクションでは、それはナンセンスです。同じことがExistsメソッドにも当てはまるためです。部外者は (たとえば で) データを操作できますが、そのような「部外者」がデータに関する特定の処理を行うには、自分自身の手段に完全に依存しなければならないdrawOneというタイプの保証があると思います。同じこと。runExistsDrawOps

スプライト/Drawables の一部は、実際にはお互いについてもっと知る必要があります。たとえば、衝突チェックやターゲット追跡などです。そのため、DrawOps またはより多くの情報を明らかにするためのドローアブルですが、今はそれを管理できるはずです。

この非常に教育的な説明をありがとう!

Exists他の好奇心旺盛な読者のために、実際のサンプルコードを以下に示します:)

module ExistsExample where

import Data.Exists
import Data.List
import Control.Monad.Eff.Console
import Control.Monad.Eff
import Prelude
import Data.Traversable

main :: forall e. Eff (console :: CONSOLE | e) Unit
main = do
    let all = updateAll $ updateAll drawables
    drawAll all

type DrawEffect = forall e. Eff (console :: CONSOLE | e) Unit

data DrawOps a = DrawOps { "data" :: a
                         , draw :: a -> DrawEffect
                         , update :: a -> a
                         }

updateInt = (+1)
updateString = (++ ".")

drawables :: List (Exists DrawOps)
drawables = fromFoldable $ [ mkExists (DrawOps { "data": 1
                                          , draw: print
                                          , update: updateInt
                                          })
                           , mkExists (DrawOps { "data": "foo"
                                          , draw: print
                                          , update: updateString
                                          })
                           ]

drawAll :: List (Exists DrawOps) -> DrawEffect
drawAll list = do
    traverse (runExists drawOne) list
    return unit
  where
    drawOne :: forall a. (DrawOps a) -> DrawEffect
    drawOne (DrawOps ops) = ops.draw ops."data"


updateAll :: List (Exists DrawOps) -> List (Exists DrawOps)
updateAll =
    map (runExists updateOne)
  where
    updateOne :: forall a. DrawOps a -> Exists DrawOps
    updateOne (DrawOps ops) = mkExists (DrawOps ( { draw: ops.draw
                                        , update: ops.update
                                        , "data": ops.update ops."data" } ))
于 2016-05-03T17:55:31.600 に答える