【问题标题】:Can the type checker help me out here? With type families, maybe?类型检查器可以帮助我吗?也许是类型家庭?
【发布时间】:2013-05-18 01:14:11
【问题描述】:

所以我写这个小soccer game 有一段时间了,有一件事从一开始就让我烦恼。游戏遵循Yampa Arcade 模式,因此游戏中的“对象”有一个求和类型:

data ObjState = Ball Id Pos Velo
              | Player Id Team Number Pos Velo
              | Game Id Score

对象对消息做出反应,因此还有另一种 sum 类型:

data Msg = BallMsg BM
         | PlayerMsg PM
         | GameMsg GM
data BM = Gained | Lost
data PM = GoTo Position | Shoot
data GM = GoalScored | BallOutOfBounds

Yampa 框架依赖于所谓的信号函数。在我们的例子中,有球、球员和比赛行为的信号函数。粗略简化:

ballObj, playerObj, gameObj :: (Time -> (GameInput, [Msg])) 
                               -> (Time -> (ObjState, [(Id, Msg)]))

例如ballObj 接受一个函数,该函数产生 GameInput(击键,游戏状态,...)和在任何给定时间专门针对球的消息列表,并返回一个函数,该函数产生球的状态和它对其他对象的消息(球,游戏,玩家)在任何给定时间。在 Yampa 中,类型签名实际上看起来更好一些:

ballObj, playerObj, gameObj :: SF (GameInput, [Msg]) (ObjState, [(Id, Msg)])

这种统一的类型签名对于 Yampa 框架很重要:(再次,非常粗略地简化)它从 11 + 11(玩家)+1(球)+1(游戏)信号函数的列表中构建了一个大信号函数然后运行(通过 reactimate)的相同类型(通过 dpSwitch)。

所以现在,让我烦恼的是:将 BallMsg 发送给 Ball 或 PlayerMsg 发送给 Player 才有意义。例如,如果有人向 Ball 发送 GameMsg,程序就会崩溃。有没有办法让类型检查器就位以避免这种情况?我最近阅读了这篇关于类型族的精彩Pokemon 帖子,似乎有一些类比。所以也许这可能是一个起点:

class Receiver a where
  Msg a :: *
  putAddress :: Msg a -> a -> Msg a

data BallObj = ...
data GameObj = ...
data PlayerObj = ...

instance Receiver BallObj where
  Msg BallObj = Gained | Lost
(...)

现在,SF 函数可能看起来像这样:

forall b . (Receiver a, Receiver b) => SF (GameInput, [Msg a]) (a, [(b, Msg b)])

这会带我去任何地方吗?

【问题讨论】:

  • 放松。 Haskell 程序不会因为类型错误而崩溃(除非您使用不安全或外来代码)。您是否有演示问题的有效代码?
  • 由于处理消息的函数中的非详尽模式而崩溃。
  • 啊,那种崩溃。
  • 我认为既然所有消息都广播给所有对象(这是真的吗?),任何对象都应该简单地忽略任何不适合它的消息。
  • 您可以使用类型族甚至(多参数)类型类来实现约束,但它不会帮助您使用单个调度函数来路由所有消息。您最终仍然需要所有消息和对象的总和类型,因为您不能拥有异构集合。您可以使用额外的类型技巧来使这些东西的创建看起来不错,但最终您将使用一些类型约束来获取消息发送的编译时间检查,以及一些用于集中消息处理的 sum 类型。

标签: haskell types


【解决方案1】:

浏览 yampa 街机纸,您似乎有一个从他们的示例中提取的 route 函数。

我的建议是你改变route,这样它就不需要一个对象列表,而是一个游戏对象、一个球对象和一组玩家对象。然后有

data BallMsg = ...
data PlayerMsg = ...
data GameMsg = ...

data AnyMsg = ABallMsg BallMsg
            | APlayerMsg PlayerMsg
            | AGameMsg GameMsg

现在route 处理统一的AnyMsg,但它会根据它们的内容将它们分派到正确的目的地。

【讨论】:

  • 感谢您的意见!我的代码已经按照您的建议进行了组织,问题是我没有在编译时使用类型检查器来防止非法对象/味精组合。我想我必须重新提出我的问题,我可以看到我想要的并不是很清楚。
【解决方案2】:

乍一看,您的设计存在一个主要问题:您将完全不同的实体BallPlayerGame 结合在一个类型下。如果您需要对这些实体使用联合类型,请采用与消息相同的方式,将它们设为单独的类型,即:

data AnyObject = AnyObjectBall Ball
               | AnyObjectPlayer Player
               | AnyObjectGame Game

这样您就可以同时表达特定功能 (Ball -> BallMsg -> ...) 和一般功能 (AnyObject -> AnyMsg -> ...)。

但如果我正确理解您的问题,我想我有一个不需要联合类型的解决方案:

class Signal object message where
  signal :: SF (GameInput, [message]) (object, [(Id, message)])

data Ball = Ball Id Pos Velo
data BallMsg = BallMsgGained | BallMsgLost
instance Signal Ball BallMsg where
  -- ...

-- so on for Player and Game

【讨论】:

  • 谢谢,应该是这样的。不过,对象和消息之间需要存在某种功能依赖或类型族关系,我认为我需要将消息与接收对象在 SF 结果中进行元组化。它仍然可能不起作用或看起来很笨拙,因为归根结底,消息生成和分发具有动态性质,无论如何可能不适合静态类型。如果我管理过一些事情,我会在这里分享!
  • @martingw 如果消息或对象的类型是在运行时定义的,那么任何基金、类型族或任何其他类型级编程特性都无法为您提供帮助。在这种情况下,我建议坚持使用 AnyObject -> AnyMsg -> ... 类型的函数进行模式匹配。
  • 我认为有一些静态类型的机会:例如,球 SF 不断测量球的位置,如果球出界,它可能会发送“出界”消息给SF,并向当前拥有球的球员发出“你不再拥有球权”的消息。假设 Game 的 ID 为 2,玩家的 ID 为 4,那么 Ball SF 返回一个列表 [(2, OutOfBounds), (4, DropPossession)]。如果我混淆了数字,程序就会崩溃。它可能与 [(Game, OutOfBounds), (Player, DropPossession)] 之类的东西有关,在 Object 和 Message 类型之间有一个fundep...
  • @martingw 扩展AnyMsg 类型以包含您放入元组的信息(id 或其他)怎么样?我的意思是data AnyMsg = ABallMsg Id BallMsg | ...,只使用AnyMsg 而不是(Id, AnyMsg)
  • Nikita,您的提案中的以下内容不适合我的问题:您在第一个参数和 SF 的第二个参数中有类型变量消息,这意味着它们是相等的。但是第一个消息的类型直接取决于对象(Ball SF 只能处理 Ball 消息),而第二个消息可以是任何类型的对象的消息。因此,我发现您需要fundeps 来解决第一个问题,并将对象和消息元组化以解决第二个问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-20
  • 2023-03-12
  • 1970-01-01
相关资源
最近更新 更多