4

问题

我正在尝试用 PureScript 编写游戏引擎。我是新手,但是自从我之前经历过 Real World Haskell 以来,学习一直很顺利(尽管我也没有太多将 Haskell 用于“真实”事物的经验)。任何将我的运行时错误尽可能多地转化为编译时错误的事情,在我的书中都是一个胜利——但如果该语言被证明过于限制我抽象问题的能力,它可以消除一些胜利。

好的,所以,我正在尝试在 HTML5 Canvas/context2d 上用 PureScript 构建一个 2d 游戏引擎(显然 purescript-canvas 是一个很好的选择 - 我更喜欢它而不是 Elm 的 Graphics.Canvas 模块,因为它映射了很多更接近实际的底层 JS API,特别是让我可以访问 Canvas 的各个像素)。

在我现有的(未完成但可用的)JS 引擎中,核心功能是我将保留一个“精灵”列表(异构,除了它们都共享一个公共类),并遍历它们以调用.update(timeDelta).draw(context2d)方法。

这些精灵都共享一个通用接口,但必须在底层支持根本不同的数据。一个可能有 x/y 坐标;另一个(可能代表环境效果)可能具有“完成百分比”或其他动画状态。

问题是,我只是想不出一个等效的抽象(异构/共享类列表)来做我需要它做的事情,而不滥用 FFI 来破解我的方式进入非常不纯的代码。

解决方案(及其问题)

异构列表 (duh)

显然,可以做异构列表等效的最佳可能抽象是异构列表。

哈斯克尔风格

事实证明,Haskell(即欺骗的 GHC,而不是官方规范/报告)提供了我想要- 你可以在仍然保持类约束的同时将类型信息装箱,在列表中的所有项目上应用单个多态函数,不破坏类型安全。这将是理想的,但可惜 PureScript 目前不允许我表达如下类型:

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

PureScript-Style(最先进的)

对于 PureScript,有purescript-exists包,它可能旨在提供与上面的 Haskell 解决方案等效的功能,并让我(不是隐藏,而是删除)类型信息,然后将其重新放入。这会让我有一个异构列表,但会以完全破坏类型安全为代价。

更重要的是,我认为我不能让它令我满意,因为即使我有一个 的列表[Exists f],我也不能只是将类型提取/重新添加为泛型forall a. (Draw a) => a——我必须知道实际的我正在恢复的类型。我可以包含某种“标签”,告诉我应该提取哪种“真实”类型,但如果我要使用这些恶作剧,我还不如用纯 JS 编码。我可能必须做的(对于列表,不一定是包含的精灵)。

一个海量数据价值中的所有状态

我可以将所有精灵统一为具有相同类型,通过在一个庞大的结构中表示各个精灵的所有状态,将其传递给每个精灵的“更新”实现(仍然不能使用类多态性,但我可以包含一个突变作为类型的一部分的每个单独的精灵值的函数,并使用它)。这很糟糕,原因很明显:每个精灵都可以自由地改变/更新其他精灵的数据。对于我必须表示的每种新的精灵状态,必须在全局范围内更新海量数据结构。无法制作它的库,因为每个使用引擎的人都必须修改它。还不如是JS。

分离的同质状态类型

或者每个精灵可以有单独的状态,并且都具有相同的状态表示。这将避免“手指在彼此的馅饼中”的情况,但我仍然有一个统一的结构,我必须通过对每个精灵的需求的过多了解来更新,对于不需要的类型结构的那些位,大量浪费的数据由每个精灵。非常糟糕的抽象。

用 JSON 表示不同的数据或者你有什么

嗯。这种方式基本上只是使用 JS 数据并假装它是 PureScript。不得不抛弃 PureScript 打字的所有优势。

没有抽象

我可以将它们都视为完全不相关的类型。这意味着如果我想添加一个新的精灵,我必须更新最外层的draw函数来为最外层的函数添加一个drawThisParticularSprite, 同上update。可能是所有可能解决方案中最糟糕的一个。

我可能会做什么

假设我对可用的抽象选择的评估是正确的,很明显我将不得不以一种或另一种方式滥用 FFI 来做我需要的事情。也许我会有一个统一的记录类型,比如

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

哪里Data有一些杂乱无章的类型删除的东西,比如某种类型的东西Exists f,和

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

或者其他的东西。和方法都特定于单个记录,并且都“知道”要从中提取的真实draw类型。updateData

与此同时,我可能会继续询问 PureScript 开发人员是否有可能支持 Haskell 风格的存在主义东西,这样我就可以在不破坏类型安全的情况下获得一个正确的、真正的异构列表。我认为主要的一点是(对于之前链接的 Haskell 示例),ShowBox必须存储其(隐藏)成员的实例信息,因此它会Show从自己的show函数覆盖中知道要使用的正确实例.

恳求

有人可以确认以上关于我目前在 PureScript 中的可用选项是否准确?我会很感激任何更正,特别是如果你发现了一种更好的方法来处理这个问题——特别是如果有一种方法允许我只使用“纯”代码而不牺牲抽象——请告诉我!

4

2 回答 2

3

我在这里假设您的Draw课程看起来像

class Draw a where
  draw :: a -> DrawEffect
  update :: a -> a

purescript-exists选项可以工作,并且它绝对是类型安全的,尽管您声称删除信息而不是隐藏它。

您需要将类上的操作移动到一个类型中:

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

现在,您想要的类型是Exists DrawOps,可以将其放入列表中,例如:

drawables :: List (Exists DrawOps)
drawables = fromArray [ mkExists (DrawOps { "data": 1
                                          , draw: drawInt
                                          , update: updateInt
                                          }
                      , mkExists (DrawOps { "data": "foo"
                                          , draw: drawString
                                          , update: updateString
                                          }
                      ]

您可以(安全地)使用 展开类型runExists,注意类型runExists强制您忽略包装数据的类型:

drawAll :: List (Exists DrawOps) -> DrawEffect
drawAll = traverse (runExists drawOne)
  where drawOne (DrawOps ops) = ops.draw ops."data"

但是,如果这些是您的类中唯一的操作,那么您可以使用同构类型

data Drawable = Drawable { drawn :: DrawEffect
                         , updated :: Unit -> Drawable
                         }

这个想法是,这种类型代表了以下操作的展开DrawOps

unfoldDrawable :: forall a. DrawOps a -> Drawable
unfoldDrawable (DrawOps ops) 
  = Drawable { drawn: ops.draw ops."data"
             , updated: \_ -> unfoldDrawable (DrawOps (ops { "data" = ops.update ops."data" })) 
             }

Drawable现在您可以使用包含不同类型数据的内容填充列表:

drawables :: List Drawable
drawables = fromArray [ unfoldDrawable 1     drawInt    updateInt
                      , unfoldDrawable "foo" drawString updateString
                      ]

同样,您可以安全地打开类型:

drawAll :: List Drawable -> DrawEffect
drawAll = traverse drawOne
  where drawOne (Drawable d) = d.drawn

updateAndDrawAll :: List Drawable -> DrawEffect
updateAndDrawAll = traverse updateAndDrawOne
  where updateAndDrawOne (Drawable d) = (d.updated unit).drawn
于 2016-05-03T03:15:36.543 回答
2

@phil-freeman(和任何其他读者):作为参考,这是我从您的答案的 Exists 部分改编的代码的完整工作版本,以供我自己验证(在底部找到)。(这是逃避评论文本长度限制的自我回答,而不是因为它是实际答案)

所以,很明显我在 Exists 工作方式的一些关键方面是错误的。我已经阅读了源代码,但是作为 PureScript 的新手,我认为我无法正确阅读 Rank-2 类型的runExists. 我听说过 Rank-N 类型,并理解它们限制了 a 的范围forall,但不明白为什么它有用——现在我明白了。:)

据我了解,它的使用 forrunExists强制其函数参数适用于 all DrawOps,而不仅仅是一些 - 这就是为什么它必须依靠DrawOps(并且它单独)具有自我意识和 DTRT 及其更新方法的原因。

我也花了一点时间才弄清楚你对非Exists示例做了什么,但我想我现在明白了。's函数的\_ -> ...定义让我有点吃惊,可能是因为我怀疑这种技术在惰性求值 Haskell 中不是必需的,但在 PureScript 中,它当然需要一个函数来防止它同时展开所有内容。Drawableupdated

我在想,也许非Exists方法是劣等的,因为它不允许任何人对数据进行操作,除了它自己......但当然在反思中,这是无稽之谈,因为Exists方法也是如此——它看起来像一个局外人能够处理数据(例如,在 中drawOne)——但我猜runExists任何这样的“局外人”必须完全依靠DrawOps自己的手段来处理与数据有关的任何事情,所以它相当于一样的东西。

一些精灵/可绘制对象实际上需要更多地了解彼此/相互交互 - 例如,碰撞检查和目标跟踪,所以我必须适当地扩展可用功能以允许 DrawOps 或Drawables 以显示更多信息,但我认为我现在应该能够管理它。

感谢您对这个极具教育意义的解释!

(工作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 回答