How do I model inheritance in Haskell?

前端 未结 3 2053
走了就别回头了
走了就别回头了 2021-01-04 19:14

I am attempting to create a game engine that is composed of a few different types:

data Camera = Camera ...
data Light = SpotLight ... | DirectionalLight ...         


        
相关标签:
3条回答
  • 2021-01-04 20:01

    I would implement it similar to:

    type Position = (Double, Double, Double)
    type Velocity = (Double, Double, Double)
    
    class PhysicalObject a where
        pos :: a -> Position
        velocity :: a -> Velocity
    
    data Camera = Camera
        { camPos :: Position
        , camVel :: Velocity
        } deriving (Eq, Show)
    
    instance PhysicalObject Camera where
        pos = camPos
        velocity = camVel
    

    Then you can do similarly for each type you define that needs PhysicalObject.

    0 讨论(0)
  • 2021-01-04 20:07

    I can think of two approaches - type classes and lenses.

    Type classes

    class PhysicalObject m where
      position :: m -> (Double, Double, Double)
      velocity :: m -> (Double, Double, Double)
    

    You would then make instances for the objects along the following lines

    data Camera = Camera 
      { cameraPosition :: (Double,Double,Double)
      , cameraVelocity :: (Double,Double,Double)
      }
    
    instance PhysicalObject Camera where
      position       = cameraPosition
      cameraVelocity = cameraVelocity
    

    and similarly for your other types. Then any function which doesn't need to know the details of an object can just require its arguments to be instances of PhysicalObject, for example:

    type TimeInterval = Double
    
    newPosition :: PhysicalObject m => TimeInterval -> m -> (Double,Double,Double)
    newPosition dt obj = (x + du * dt, y + dv * dt, z + dw * dt)
     where
      (x,y,z) = position obj
      (u,v,w) = velocity obj
    

    However, you will struggle to write functions which modify your objects using this code - the class tells Haskell how it can access the position and velocity of an object, but not how to modify them.

    Lenses

    The other option is to turn to the lens library. This is a bit of a beast to being with, but it allows you to write some very natural code. First, there's a bit of boilerplate

    {-# LANGUAGE TemplateHaskell #-}
    import Control.Lens
    

    Now define some position and velocity data types. Don't worry about the weird field names prefixed with underscores - we won't be using them.

    data Pos = Pos { _posX, _posY, _posZ :: Double }
    data Vel = Vel { _velX, _velY, _velZ :: Double }
    
    instance Show Pos where show (Pos x y z) = show (x,y,z)
    instance Show Vel where show (Vel x y z) = show (x,y,z)
    

    Now you use a bit of Template Haskell to derive lenses for your data types. This will generate type classes HasPos and HasVel whose methods allow you to access and modify any value that is an instance of those classes.

    makeClassy ''Pos
    makeClassy ''Vel
    

    Now define your camera class, which includes a position and a velocity.

    data Camera = Camera
      { _cameraPos :: Pos
      , _cameraVel :: Vel } deriving (Show)
    

    Another bit of Template Haskell will automatically create functions cameraPos and cameraVel that allow you to access and modify the position and velocity of your camera.

    makeLenses ''Camera
    

    Finally, declare that your camera is an instance of both the HasPos and HasVel classes, with a default implementation of their methods.

    instance HasPos Camera where pos = cameraPos
    instance HasVel Camera where vel = cameraVel
    

    Now we're ready to do some real work. Let's define an example camera

    camera = Camera (Pos 0 0 0) (Vel 10 5 0)
    

    A function to modify the camera, returning a new one with an updated position, is

    move :: (HasPos a, HasVel a) => TimeInterval -> a -> a
    move dt obj = obj
      & posX +~ dt * obj^.velX
      & posY +~ dt * obj^.velY
      & posZ +~ dt * obj^.velZ
    

    Note that this is a completely generic function for moving any object that has a position and velocity - it's not at all specific to the Camera type. It also has the advantage of looking a lot like imperative code!

    If you now load all this into GHCI, you can see it in action

    >> camera
    Camera {_cameraPos = (0.0,0.0,0.0), _cameraVel = (10.0,5.0,0.0)}
    >> move 0.1 camera
    Camera {_cameraPos = (1.0,0.5,0.0), _cameraVel = (10.0,5.0,0.0)}
    
    0 讨论(0)
  • 2021-01-04 20:21

    You'll want to begin to depend on things like typeclasses and object encodings. The first method is to encode the common interface as a typeclass each type inherits from.

    class PhysicalObject o where
      pos      :: o -> Vector3
      velocity :: o -> Vector3
    

    The second is to build a common object

    data PhysicalObject = PhysicalObject { poPos :: Vector3, poVelocity :: Vector3 }
    
    data Monster = Monster { monsterPO :: PhysicalObject
                           , ... monsterStuff ...
                           }
    

    which could even be used to instantiate the first typeclass

    instance PhysicalObject PhysicalObject where
      pos      = poPos
      velocity = poVelocity
    
    instance PhysicalObject Monster where
      pos      = pos      . monsterPO
      velocity = velocity . monsterPO
    

    Be careful with typeclass encodings like this, though, as too great a use of them often causes ambiguity when reading code. It can be difficult to understand the types and know which instance is being used.

    0 讨论(0)
提交回复
热议问题