不同数据类型的Haskell模式匹配

3
我正在创建一个“Asteroids”的克隆版,并想要创建一个移动函数。我原以为可以在数据类型上使用模式匹配,但实际上类型签名与实际方法不符合。如果move函数中的Moving数据类型中的参数tBullet数据类型,则我想使用不同的代码并尝试了这个方法,但它不起作用。除了制作专门的移动功能(这可能更好),还有其他想法吗?

因此,我有Moving AsteroidMoving Bullet,并且想要根据AsteroidBullet的类型进行模式匹配(或其他未在此处发布的类型,以提供最小示例)

一句话描述move函数应该做什么:对于所有类型的Moving o,使用环绕移动,除了Moving Bullet

一些上下文代码:

data Moving s = Moving {
                    position :: Position,
                    velocity :: Velocity,
                    size :: Float, 
                    specifics :: s
}

data Bullet = Bullet {
                damage :: Int
              }
            | DeadBullet

data Asteroid = Asteroid  
              | DeadAsteroid

move :: Float -> Moving o -> Moving o
move secs (Moving (x, y) v@(vx, vy) s t@(Bullet _)) = Moving (x', y') v s t
  where x' = (x + vx * secs)
        y' = (y + vy * secs)

move secs (Moving (x, y) v@(vx, vy) s t) = Moving (x', y') v s t
  where x' = (x + vx * secs) `mod'` width
        y' = (y + vy * secs) `mod'` height

错误:

src\Controller.hs:100:42: error:
    * Couldn't match expected type `o' with actual type `Bullet'
3个回答

4
你不能这样进行模式匹配,因为Moving o是多态的。如果你有一个只移动子弹的函数,Moving Bullet就可以像这样进行模式匹配。
有很多不同的方法可以解决这个问题。根据你游戏中的其他方面,一种简单的解决方案是将BulletAsteroid合并成一个单一的Movable数据类型,然后对其进行模式匹配:
data Moving = Moving {
                    position :: Position,
                    velocity :: Velocity,
                    size :: Float, 
                    specifics :: Movable
}

data Movable = B Bullet | A Asteroid 

data Bullet = Bullet {
                damage :: Int
              }
            | DeadBullet

data Asteroid = Asteroid  
              | DeadAsteroid 

move :: Float -> Moving -> Moving
move secs (Moving (x, y) v@(vx, vy) s t@(B _)) = Moving (x', y') v s t
  where x' = (x + vx * secs)
        y' = (y + vy * secs)

move secs (Moving (x, y) v@(vx, vy) s t) = Moving (x', y') v s t
  where x' = (x + vx * secs) `mod'` width
        y' = (y + vy * secs) `mod'` height

这种方法意味着我能把现在的“移动子弹”和“移动小行星”放到一个数组/列表中吗?那看起来太棒了。 - The Coding Wombat
1
是的!现在你可以拥有一个[可移动],因为它们被封装成了一个单一类型。同样地,由于Moving不再具有多态性,你可以拥有包含BulletAsteroid[Moving] - jkeuhlen

3
我不反对jkeuhlen(或你在第一个问题中提到的chepner)的观点:你可能根本不需要类型区分,可以全部保留在值层面。
但你也可以在类型层面上做到这一点,因为移动对象不应该改变其类型。这是现在你会使用类的地方。你可以将move作为一个方法:
type Time = Float

class ObjSpecific s where
  move :: Time -> Moving s -> Moving s

instance ObjSpecific Bullet where
  move δt (Moving p v s t) = -- definition without edge-wrapping
instance ObjSpecific Asteroid where
  move δt (...) = ... -- definition with edge-wrapping

顺便说一下,我认为你应该做些什么来消除子弹离开屏幕后的问题...也许可以使用move :: Time -> Moving s -> Maybe (Moving s)


移动函数的类型不应该是 move :: Float -> Moving s -> Moving s 吗? - lsmor
谢谢您的建议,但为了创建一个最小化的工作示例,我省略了它。现在我会采用@Jkeuhlen的方法。但我肯定会研究这种解决问题的方式,之前您也建议我使用类来解决这些问题。 - The Coding Wombat

2
除了jkeuhlen的答案外,您还可以使用类型类:
class Moveable a where
    move :: Float -> a -> a
    position :: a -> Position
    velocity :: a -> Velocity

data Asteroid = Asteroid {
      asteroidP :: Position,
      asteroidV :: Velocity
   }

instance Moveable Asteroid where
    move secs (Asteroid (x, y) v@(vx, vy)) = 
       Asteroid ((x + secs*vx) `mod'` width, (y + secs*vy) `mod'` height) v
    position = asteroidP
    velocity = asteroidV

同样的,对于 Bullet 也是如此。

这看起来类似于面向对象的继承方法,你可能已经熟悉了。然而请注意,代码中的 Moveable 是一组类型,但它本身不是一个类型。你不能创建一个 Moveable 物品列表并将陨石和子弹放入其中。子弹和陨石仍然保持不同的类型。如果你想把它们都放在一个列表中,那么你必须使用 jkeulen 的方法(当然,将两者结合起来也没有问题)。


这两种实现方式中的一种比另一种更好吗?(编辑:这可能在游戏的其余实现方面有很大关系) - The Coding Wombat
1
是的,这就是为什么我添加了最后一段。如果您有所有移动对象的列表,则jkeuhlen的方法会更好。 - Paul Johnson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接