我试图用PureScript编写游戏引擎。我是新手,但学习一直顺利,因为我以前经历过真实世界的Haskell(尽管我在使用Haskell和#34;真实"事情方面有很多经验)。任何将我的运行时错误尽可能多地移动到编译时错误中的东西,在我的书中都是一个胜利 - 但如果语言被证明对我抽象出问题的能力过于严格,那么它可以取消一些胜利。
好吧,所以,我试图在HTML5 Canvas / context2d上用PureScript构建一个2D游戏引擎(显然purescript-canvas是一个很好的选择 - 我更喜欢它而不是Elm&s;的图形.Canvas模块,因为它映射得非常接近实际的底层JS API,特别是让我可以访问Canvas的各个像素。
在我现有的(未完成但可用的)JS引擎中,核心功能是我会保留一份" sprites" (异构,除了它们都共享一个公共类),并循环遍历它们以调用.update(timeDelta)
和.draw(context2d)
方法。
精灵们共享一个共同的界面,但必须支持根本不同的数据。一个可能有x / y坐标;另一个(代表可能是环境影响)可能有一个完整的百分比"或其他动画状态。
问题是,我不能提出一个等同的抽象(到异构/共享类列表)来完成我需要它做的事情,而不会滥用FFI来破解我的方式进入非常不纯的代码。
显然,可以做到相当于异构列表的最佳抽象是异构列表。
事实证明Haskell(即被欺骗的GHC,而不是官方规范/报告)提供exactly what I want - 你可以在保持类约束的同时收集类型信息,在所有项目中应用单个多态函数在列表中,没有打破类型安全。这将是理想的,但是唉,PureScript目前还不允许我表达类似的类型:
data ShowBox = forall s. Show s => SB s
对于PureScript,有purescript-exists包,它可能旨在提供与上面的Haskell解决方案相同的功能,并且让我 - 不要隐藏,但删除类型信息,并再次将其重新输入。这将让我有一个异类列表,但代价是完全打破类型安全。
更重要的是,我不认为我可以让它让我满意,因为即使我有[Exists f]
的列表,我也无法提取/重新添加作为通用forall a. (Draw a) => a
的类型 - 我必须知道I' m恢复的实际类型。我可以包含一个"标签"某种形式告诉我什么"真实"我应该提取的类型,但如果我拉这些恶作剧我也可以用普通的JS编码。我可能需要做的事情(对于列表,不一定是包含的精灵)。
我可以通过在一个巨大的结构中表示各个精灵的所有状态,将它传递给每个精灵来更新"实现(仍然不能使用类多态,但我可以为每个sprite值包含一个变异函数作为类型的一部分,并使用它)。这很糟糕:每个精灵都可以自由地改变/更新其他精灵的数据。对于我必须表示的每种新的精灵状态,必须全局更新海量数据结构。无法创建它的库,因为使用引擎的每个人都必须修改它。也可能是JS。
或者每个精灵可以有单独的状态,并且都具有相同的状态表示。这样可以避免彼此之间的关注。馅饼"方案,但我仍然有一个统一的结构,我必须更新每个精灵的需要,对每个精灵不需要的类型结构的那些位的大量浪费数据。非常糟糕的抽象。
Ew的。这种方式基本上 只是使用JS数据并假装它的PureScript。必须抛弃PureScript键入的所有优势。
我可以将它们视为完全不相关的类型。这意味着如果我想添加一个新的精灵,我必须更新最外面的draw
函数,为最外面的drawThisParticularSprite
函数添加update
,同上。可能是所有可能解决方案中最差的。
假设我对我可用的抽象选择的评估是正确的,似乎很明显我不得不以某种方式滥用FFI来做我需要的事情。也许我会有像
这样的统一记录类型type Sprite = { data: Data, draw: Data -> DrawEffect, update: Data -> Data }
其中Data
是某种类似于kludgey类型的东西,可能是某种类型的Exists f
,并且
type DrawEffect = forall e. Eff (canvas :: Canvas | e) Context2D
或者其他什么。 draw
和update
方法都是针对各个记录的,并且两者都知道"要从Data
中提取的真实类型。
与此同时,我可能会向PureScript开发人员询问是否有可能支持Haskell风格的存在性东西,这样我就可以在不破坏类型安全的情况下获得一个合适的,真正的异构列表。我认为主要的一点是(对于Haskell example previously linked),ShowBox
必须存储其(隐藏)成员的实例信息,因此它知道{{{}的正确实例1}}使用,来自它自己的Show
函数的覆盖。
有人可以确认以上在PureScript中我目前的可用选项是否准确吗?我很欣赏任何更正,特别是如果你看到一个更好的方法来处理这个问题 - 特别是如果有一个允许我只使用"纯粹"代码而不牺牲抽象 - 请告诉我!
答案 0 :(得分: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
答案 1 :(得分:2)
@ phil-freeman(以及任何其他读者):作为参考,这里是我从你的答案的Exists部分改编的代码的完整,有效的版本,以便为我自己验证(在底部找到)。 (这是一个逃避评论文本长度限制的自我答案,而不是因为它是一个真正的答案)
所以,似乎很明显我错误地关注了Exists如何工作的一些关键方面。我已经阅读了源代码,但对于PureScript的新手,我认为我无法正确阅读Rank {2类型的runExists
。我听说过Rank-N类型,并且理解它们限制了forall
的范围,但是不明白为什么它有用 - 现在我做了。 :)
据我了解,它对runExists
的使用迫使其函数参数适用于所有DrawOps
,而不仅仅是某些 - 这就是为什么它必须依赖于DrawOps
(并且它本身就是自我意识和DTRT及其更新方法。
我还花了一些时间来弄清楚你在非Exists
例子中做了些什么,但我想我现在就明白了。我被\_ -> ...
Drawable
函数的updated
定义抛出了一点,可能是因为我怀疑这种技术在懒惰评估Haskell时不是必需的,但在PureScript中当然是它需要是一个功能,以防止它一下子展开一切。
我在想,也许非Exists
方法是劣等的,因为它不允许任何人对数据进行操作,除了它自己......但当然在反思时,这是无稽之谈,因为同样如此Exists
方法 - 它看起来就像一个局外人能够使用数据(例如,drawOne
) - 但我想{{1}的类型保证任何这样的“局外人”必须完全依赖runExists
自己的方式来处理有关数据的任何具体内容,因此它们的含义相同。
一些精灵/抽象实际上需要彼此了解更多/互相交流 - 例如,碰撞检查和目标跟踪,所以我必须适当地扩展可用功能以允许DrawOps或Drawables以显示更多信息,但我认为我现在应该能够管理它。
感谢您提供这种非常有教育意义的解释!
(其他好奇的读者可以使用DrawOps
示例代码:)
Exists