适当的抽象代替异构(但共享-class)列表?
Appropriate abstraction in lieu of heterogenous (but shared-class) list?
问题
我正在尝试用 PureScript 编写游戏引擎。我是新手,但是自从我之前经历过 Real World Haskell 以来学习一直很顺利(尽管我也没有太多使用 Haskell 来处理 "real" 事情的经验) .任何将我的运行时错误尽可能多地转化为编译时错误的东西,在我看来都是一个胜利——但如果这种语言被证明过度限制了我抽象出问题的能力,它可以消除一些胜利。
好的,所以,我正在尝试在 HTML5 Canvas/context2d 上用 PureScript 构建一个 2d 游戏引擎(显然 purescript-canvas 是一个很好的选择 - 我比 Elm 的 Graphics.Canvas 模块更喜欢它,因为它与实际的底层 JS API 映射得更紧密,特别是让我可以访问 Canvas 的各个像素)。 =32=]
在我现有的(未完成但可用的)JS 引擎中,核心功能是我会保留一个 "sprites" 的列表(异构的,除了它们都共享一个公共的 class),并循环遍历它们以调用 .update(timeDelta)
和 .draw(context2d)
方法。
精灵都共享一个通用界面,但必须在后台支持根本不同的数据。一个可能有 x/y 个坐标;另一个(可能代表环境影响)可能具有 "percent complete" 或其他动画状态。
问题是,我无法想出一个等效的抽象(对 heterogeneous/shared-class 列表)来完成我需要它做的事情,而不滥用 FFI 来侵入我的方式非常不纯的代码。
解决方案(及其问题)
异构列表(废话)
显然,可以等效于异构列表的最佳抽象是异构列表。
Haskell-样式
事实证明 Haskell(即伪造的 GHC,不是官方 spec/report)提供 exactly what I want - 您可以在保留 [=114 的同时打包类型信息=] 约束,在列表中的所有项目上应用单个多态函数,而不会破坏类型安全。这将是理想的,但是 PureScript 目前不允许我表达这样的类型:
data ShowBox = forall s. Show s => SB s
PureScript 风格(最先进的)
对于 PureScript,有 purescript-exists 包,它可能旨在提供与上面 Haskell 解决方案等效的功能,并且让我——不是隐藏,而是 remove——输入信息,重新输入。这会让我有一个异构列表,但以完全破坏类型安全为代价。
更重要的是,我认为我无法使它的工作令我满意,因为即使我有 [Exists f]
的列表,我也不能只是 extract/re-add 类型作为通用 forall a. (Draw a) => a
——我必须知道我要恢复的实际类型。我可以包含某种 "tag" 类型,告诉我应该提取哪种 "real" 类型,但如果我要提取这些类型的恶作剧,我还不如用纯 JS 编码。我可能必须这样做(对于列表,不一定包含精灵)。
一个海量数据值中的所有状态
我可以将所有精灵统一为同一类型,方法是在一个巨大的结构中表示单个精灵的所有状态,将其传递给每个精灵的 "update" 实现(仍然不能使用 class 多态性,但我可以为每个单独的精灵值包含一个突变函数作为类型的一部分,并使用它)。这很糟糕,原因很明显:每个精灵都可以自由 mutate/update 其他精灵的数据。对于我必须表示的每种新的精灵状态,必须在全球范围内更新海量数据结构。不能把它做成一个库,因为每个使用引擎的人都必须修改它。也可能是JS。
独立的同质状态类型
或者每个精灵都可以有单独的状态,并且都具有相同的状态表示。这将避免 "fingers in each others' pies" 场景,但我仍然有一个统一的结构,我必须更新每个精灵的需求的过多知识,每个精灵不需要的类型结构的那些位的大量浪费数据.抽象性很差。
表示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
和 update
方法都特定于单个记录,"know" 都是从 Data
.
中提取的真实类型
与此同时,我可能会继续询问 PureScript 开发人员有关支持 Haskell 风格存在主义内容的可能性,这样我就可以在不破坏类型安全的情况下获得适当的、真正的异构列表。我认为主要的一点是(对于 Haskell example previously linked),ShowBox
必须存储其(隐藏)成员的实例信息,因此它知道 [=27= 的正确实例] 来使用,从它自己对 show
函数的覆盖。
请求
有人可以确认以上关于我目前在 PureScript 中的可用选项是否准确吗?如果您有任何更正,我将不胜感激,特别是如果您发现了一种更好的方法来处理这个问题——尤其是如果有一种方法允许我只使用 "pure" 代码而不牺牲抽象——请告诉我!
我在这里假设你的 Draw
class 看起来像
class Draw a where
draw :: a -> DrawEffect
update :: a -> a
purescript-exists
选项可以工作,而且它绝对是类型安全的,尽管您声称要删除信息而不是隐藏信息。
您需要将 class 上的操作移动到一个类型中:
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"
但是,如果这些是您 class 中唯一的操作,那么您可以使用同构类型
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
@phil-freeman(和任何其他读者):作为参考,这是我从你的答案的存在部分改编的代码的完整工作版本,以供我自己验证(在底部找到). (这是一个自我回答,以避开评论的文本长度限制,而不是因为它是一个实际的答案)
所以,很明显我在 Exists 工作原理的一些关键方面是错误的。我已经阅读了源代码,但作为 PureScript 的新手,我认为我无法正确阅读 runExists
的 Rank-2 类型。我听说过 Rank-N 类型,并且了解它们限制了 forall
的范围,但不明白为什么这有用 — 现在我明白了。 :)
据我了解,它用于 runExists
会强制其函数参数适用于所有 DrawOps
,而不仅仅是某些——这就是为什么它必须依赖于 DrawOps
(而且只有它)具有自我意识和 DTRT 及其更新方法。
我也花了一点时间才弄明白你用非 Exists
示例做了什么,但我想我现在明白了。 Drawable
的 updated
函数的 \_ -> ...
定义让我有些吃惊,可能是因为我怀疑这种技术在惰性求值 Haskell 中不是必需的,但是在 PureScript 中,当然它需要一个函数来防止它同时展开所有内容。
我在想,也许非Exists
方法是次等的,因为它不允许任何人对除它自己之外的数据进行操作...但是当然反思,那是废话,因为同样Exists
方法是正确的——它 看起来 就像一个局外人能够玩弄数据(在,比方说,drawOne
)——但我猜类型runExists
保证任何此类 "outsider" 必须完全依赖 DrawOps
自己的方法来处理有关数据的任何特定内容,因此它们等同于同一件事。
一些 sprites/Drawables 实际上需要更多地了解每个 other/interact - 例如,碰撞检查和目标跟踪,所以我必须扩展适当地允许 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" } ))
问题
我正在尝试用 PureScript 编写游戏引擎。我是新手,但是自从我之前经历过 Real World Haskell 以来学习一直很顺利(尽管我也没有太多使用 Haskell 来处理 "real" 事情的经验) .任何将我的运行时错误尽可能多地转化为编译时错误的东西,在我看来都是一个胜利——但如果这种语言被证明过度限制了我抽象出问题的能力,它可以消除一些胜利。
好的,所以,我正在尝试在 HTML5 Canvas/context2d 上用 PureScript 构建一个 2d 游戏引擎(显然 purescript-canvas 是一个很好的选择 - 我比 Elm 的 Graphics.Canvas 模块更喜欢它,因为它与实际的底层 JS API 映射得更紧密,特别是让我可以访问 Canvas 的各个像素)。 =32=]
在我现有的(未完成但可用的)JS 引擎中,核心功能是我会保留一个 "sprites" 的列表(异构的,除了它们都共享一个公共的 class),并循环遍历它们以调用 .update(timeDelta)
和 .draw(context2d)
方法。
精灵都共享一个通用界面,但必须在后台支持根本不同的数据。一个可能有 x/y 个坐标;另一个(可能代表环境影响)可能具有 "percent complete" 或其他动画状态。
问题是,我无法想出一个等效的抽象(对 heterogeneous/shared-class 列表)来完成我需要它做的事情,而不滥用 FFI 来侵入我的方式非常不纯的代码。
解决方案(及其问题)
异构列表(废话)
显然,可以等效于异构列表的最佳抽象是异构列表。
Haskell-样式
事实证明 Haskell(即伪造的 GHC,不是官方 spec/report)提供 exactly what I want - 您可以在保留 [=114 的同时打包类型信息=] 约束,在列表中的所有项目上应用单个多态函数,而不会破坏类型安全。这将是理想的,但是 PureScript 目前不允许我表达这样的类型:
data ShowBox = forall s. Show s => SB s
PureScript 风格(最先进的)
对于 PureScript,有 purescript-exists 包,它可能旨在提供与上面 Haskell 解决方案等效的功能,并且让我——不是隐藏,而是 remove——输入信息,重新输入。这会让我有一个异构列表,但以完全破坏类型安全为代价。
更重要的是,我认为我无法使它的工作令我满意,因为即使我有 [Exists f]
的列表,我也不能只是 extract/re-add 类型作为通用 forall a. (Draw a) => a
——我必须知道我要恢复的实际类型。我可以包含某种 "tag" 类型,告诉我应该提取哪种 "real" 类型,但如果我要提取这些类型的恶作剧,我还不如用纯 JS 编码。我可能必须这样做(对于列表,不一定包含精灵)。
一个海量数据值中的所有状态
我可以将所有精灵统一为同一类型,方法是在一个巨大的结构中表示单个精灵的所有状态,将其传递给每个精灵的 "update" 实现(仍然不能使用 class 多态性,但我可以为每个单独的精灵值包含一个突变函数作为类型的一部分,并使用它)。这很糟糕,原因很明显:每个精灵都可以自由 mutate/update 其他精灵的数据。对于我必须表示的每种新的精灵状态,必须在全球范围内更新海量数据结构。不能把它做成一个库,因为每个使用引擎的人都必须修改它。也可能是JS。
独立的同质状态类型
或者每个精灵都可以有单独的状态,并且都具有相同的状态表示。这将避免 "fingers in each others' pies" 场景,但我仍然有一个统一的结构,我必须更新每个精灵的需求的过多知识,每个精灵不需要的类型结构的那些位的大量浪费数据.抽象性很差。
表示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
和 update
方法都特定于单个记录,"know" 都是从 Data
.
与此同时,我可能会继续询问 PureScript 开发人员有关支持 Haskell 风格存在主义内容的可能性,这样我就可以在不破坏类型安全的情况下获得适当的、真正的异构列表。我认为主要的一点是(对于 Haskell example previously linked),ShowBox
必须存储其(隐藏)成员的实例信息,因此它知道 [=27= 的正确实例] 来使用,从它自己对 show
函数的覆盖。
请求
有人可以确认以上关于我目前在 PureScript 中的可用选项是否准确吗?如果您有任何更正,我将不胜感激,特别是如果您发现了一种更好的方法来处理这个问题——尤其是如果有一种方法允许我只使用 "pure" 代码而不牺牲抽象——请告诉我!
我在这里假设你的 Draw
class 看起来像
class Draw a where
draw :: a -> DrawEffect
update :: a -> a
purescript-exists
选项可以工作,而且它绝对是类型安全的,尽管您声称要删除信息而不是隐藏信息。
您需要将 class 上的操作移动到一个类型中:
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"
但是,如果这些是您 class 中唯一的操作,那么您可以使用同构类型
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
@phil-freeman(和任何其他读者):作为参考,这是我从你的答案的存在部分改编的代码的完整工作版本,以供我自己验证(在底部找到). (这是一个自我回答,以避开评论的文本长度限制,而不是因为它是一个实际的答案)
所以,很明显我在 Exists 工作原理的一些关键方面是错误的。我已经阅读了源代码,但作为 PureScript 的新手,我认为我无法正确阅读 runExists
的 Rank-2 类型。我听说过 Rank-N 类型,并且了解它们限制了 forall
的范围,但不明白为什么这有用 — 现在我明白了。 :)
据我了解,它用于 runExists
会强制其函数参数适用于所有 DrawOps
,而不仅仅是某些——这就是为什么它必须依赖于 DrawOps
(而且只有它)具有自我意识和 DTRT 及其更新方法。
我也花了一点时间才弄明白你用非 Exists
示例做了什么,但我想我现在明白了。 Drawable
的 updated
函数的 \_ -> ...
定义让我有些吃惊,可能是因为我怀疑这种技术在惰性求值 Haskell 中不是必需的,但是在 PureScript 中,当然它需要一个函数来防止它同时展开所有内容。
我在想,也许非Exists
方法是次等的,因为它不允许任何人对除它自己之外的数据进行操作...但是当然反思,那是废话,因为同样Exists
方法是正确的——它 看起来 就像一个局外人能够玩弄数据(在,比方说,drawOne
)——但我猜类型runExists
保证任何此类 "outsider" 必须完全依赖 DrawOps
自己的方法来处理有关数据的任何特定内容,因此它们等同于同一件事。
一些 sprites/Drawables 实际上需要更多地了解每个 other/interact - 例如,碰撞检查和目标跟踪,所以我必须扩展适当地允许 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" } ))