2016-11-02 2 views
2

Я использую Haskell для создания игры (это задание, поэтому не судите меня), но у меня возникает проблема с типами данных.Абстрактные записи или интерфейсы записи?

Так что я хочу, это тип данных Entity, который имеет местоположение, скорость, угол и скорость вращения. Запись очень хорошо подходит для этой идеи:

data Entity = Entity { 
    location :: Vector, 
    velocity :: Vector, 
    angle :: Float, 
    rotation :: Float 
} 

Теперь мне нужны экземпляры Entity, а именно Player Rock Pickup и Bullet. Но у игроков Rocks и Bullets должно быть дополнительное поле, а именно health :: Int, а Pickup должен иметь другое дополнительное поле, а именно pickupType :: PickupType.

Но у меня есть определенные методы, которые я хочу использовать для любого типа Entity. Например:

move :: Entity -> Entity 
move [email protected](Entity {location, velocity, angle, rotation}) = e {location = location + velocity, angle = angle + rotation} 

Я понятия не имею, как это сделать или если это возможно. Я бы не понял, почему, если это невозможно, так как это возможно на других языках.

Некоторые попытки, и почему они не совсем то, что я хочу:

Попытка 1:

type Player = Player { 
    e  :: Entity, 
    health :: Int 
} 

Это работает, но это действительно некрасиво. Это, например, то, как вы перемещаете Игрок:

movePlayer :: Player -> Player 
movePlayer [email protected](e) = p {e = move e} 

Это просто действительно уродливо.

Положительные: Простота создания абстрактного класса. Простота создания экземпляров. Простые абстрактные методы.

Отрицательные: Трудно получить или задать объекты, реализуемые сущностью.

Попытка 2:

class Entity e where 
    getLocation :: e -> Vector 
    getVelocity :: e -> Vector 
    ... 
    setLocation :: Vector -> e -> e 
    setVelocity :: Vector -> e -> e 
    ... 

data Player = Player { 
    playerLocation :: Vector, 
    playerVelocity :: Vector, 
    ... 
    playerHealth :: Int 
} 

instance Entity Player where 
    getLocation = location 
    getVelocity = velocity 
    ... 
    setLocation l e = e {location = l} 
    setVelocity v e = e {playerVelocity = v} 
    ... 

move :: (Entity e) => e -> e 
move e = (setLocation (getLocation e + getVelocity e) . setAngle (getAngle e + getRotation e)) e 

Ну это работает, но я надеюсь, что мы все можем согласиться, что их определения теперь действительно некрасиво. Абстрактные методы, которые работают на любой Entity, также становятся уродливыми. Единственное, что нравится movePlayer, - это очень просто.

movePlayer :: Player -> Player 
movePlayer = move 

Мне больше не нужно определять movePlayer, так как я могу просто использовать move.

Положительные стороны: Легко получить или задавать области экземпляра экземпляра объекта.

Отрицательные: Трудно создать абстрактный класс. Еще труднее создавать экземпляры. Твердые абстрактные методы.

Покушение 3:

Дайте Entity все поля, необходимые любой экземпляр.

data Entity = Entity { 
    location :: Vector, 
    velocity :: Vector, 
    angle  :: Float, 
    rotation :: Float, 
    health  :: Int, 
    pickupType :: PickupType 
} 

Таким образом, мне даже не нужно определять экземпляры, и я могу просто использовать Entity. Единственная проблема заключается в том, что у вас много избыточных данных.В настоящее время я использую и IMO - лучшее решение для моей проблемы, но мне все еще не нравится.

Положительные: Легко создать абстрактный класс, даже если он не является абстрактным. Не нужно указывать экземпляры. Простые абстрактные методы. Простота получения или установки полей, реализуемых сущностью экземпляра.

Отрицательные: Много неиспользованных данных. Каждый раз, когда вы создаете Entity, вам нужно определить множество бессмысленных полей.

Так что, пожалуйста, помогите мне, я не могу найти более эффективные методы, чем эти три :(

+3

Последний метод по своей сути плохой, потому что слишком много полей всегда подразумевают несогласованное состояние. – ThreeFx

+3

Кроме того, нет ничего плохого в использовании Haskell для написания игр :) – duplode

ответ

2

Я бы с первой попытки, по очень простой причине:

Это точно отражает намерение Player - это в Entity с дополнительной информацией

data Player = Player { 
    e  :: Entity 
    health :: Int 
} 

Хотя все функции, касающиеся может быть громоздким, чтобы писать на первых, вы, вероятно, никогда не придется увидеть их снова, то есть вы предоставили. достаточно абстрактного интерфейса в вашем коде, чтобы напрямую не получить доступ к состоянию Player.

movePlayer :: Player -> Player 
movePlayer [email protected](e) = p {e = move e} 

Эта функция написана один раз, а затем в идеале вам больше не придется иметь дело с внутренними элементами.

Кроме того, теперь вы можете использовать классы типа, поскольку они предназначены: вы можете абстрактные movePlayer в отдельный класс типа, такие как Movable:

class Movable m where 
    move :: m -> m 

-- Obviously, you can move entities 
instance Movable Entity where 
    move e = -- stuff 

Но теперь это очень легко двигаться Players тоже:

instance Movable Player where 
    move (Player entity health) = Player (move entity) health 
    -- works, since `Entity` is movable 

Это в сторону, ваш класс подход типа имеет существенный недостаток: насчет функций а Player имеет но Entity нет? В этом случае вам придется Player наследовать Entity, как это:

class Entity e => Player e where 
    -- stuff ... 

Но так как классы типов Haskell открыты, все, что может стать Player, который не то, как это должно работать (если, конечно, это не является ваше намерение).

+2

Если есть разные типы игроков (например, для разных вариантов игры или, возможно, даже для ПК или NPC), то класс для них может иметь смысл , – dfeuer

0

Вы могли бы просто использовать сумму (иначе называемую «union»).

data Entity = 
    Player { 
     location :: Vector, 
     -- etc. 
     health :: Int } 
    | Pickup { 
     location :: Vector, 
     -- etc. 
     pickupType :: PickupType} 

Вы можете это исключить, просто имея тип суммы, удерживая данные, которые различаются.

Это имеет то преимущество, что вы можете иметь [Entity], чего вы не можете сделать, когда все разные вариации Entity являются разными типами (в отличие от языков OO).

В зависимости от вашей игровой модели вы также можете отделить данные о местоположении и скорости от остальной информации о игроках. Могли ли эти данные лучше удерживаться в какой-то структуре пространственных данных, такой как квадрант? Таким образом, вы можете хранить постоянные данные отдельно от материала, который изменяется с каждым фреймом.

. Есть одна вещь, которую вы должны рассмотреть: lenses, которые существуют, чтобы решить проблему, которую вы описываете, с множеством геттеров и сеттеров.

+2

Тип суммы - это хорошее решение, но реализовано таким образом, что у него есть недостаток в том, чтобы делать вещи наподобие «здоровье» и «pickupType». Факторинг по-разному с дополнительным слоем конструкторов был бы немного более громоздким, но более безопасным. – duplode

+0

Кроме того, разметка полей, специфичных для «Entity», имеет дополнительный бонус для уменьшения дублирования кода при добавлении дополнительного типа «Entity». – ThreeFx

0

Это более длинный комментарий, чем ответ, поскольку ядро ​​того, что я собираюсь писать, достаточно хорошо освещено ThreeFx's answer.

Во-первых, избегайте использования жаргона ООП, так как это приведет к путанице. В вашей попытке №1 Entity - это тип данных, а не класс, и он не является абстрактным. Взаимосвязь между Player и Entity в попытке № 1 является лишь одним из элементов: Player имеет номер Entity, и это никоим образом не является экземпляром Entity.

Во-вторых, запись синтаксиса обновления является довольно уродливым в Haskell. Это не усложняет ситуацию, просто немного громоздко. Поэтому, когда вы говорите, например, «Трудно получить или установить Entity-реализованные поля экземпляра», на самом деле это не сложно, просто не слишком красиво. Это не является серьезной проблемой для определения дизайна ваших типов данных.

В-третьих, объективы - это способ (среди многих других) избежать уродства синтаксиса обновления записи. Вы, вероятно, не захотите нырять в это прямо сейчас (подождите по крайней мере, пока вы не закончите свое задание), но я не могу удержаться от ссылки на очень релевантный учебник для вас, чтобы вы могли прочитать в какой-то момент в будущем: Program imperatively using Haskell lenses ,

2

Я бы сказал, что ваша первая попытка - это путь, по тем же причинам, что и в ответе @ ThreeFx. Однако я собираюсь предложить немного другую альтернативу.

Учитывая эти типы:

data Player = Player { 
    playerEntity :: Entity, 
    health  :: Int 
} 

data Pickup = Pickup { 
    pickupEntity :: Entity, 
    pickupType :: PickupType 
} 

Вместо того, чтобы иметь отдельный класс типа для каждого действия, которое может быть сделано на Entity, мы можем предоставить общие функции высшего порядка, чтобы сделать его легче выполнять Entity действий по Player s и s Pickup:

overPlayerEntity :: (Entity -> Entity) -> Player -> Player 
overPlayerEntity fn (Player pe h) = Player (fn pe) h 

overPickupEntity :: (Entity -> Entity) -> Pickup -> Pickup 
overPickupEntity fn (Pickup pe t) = Pickup (fn pe) t 

Теперь мы можем иметь

movePlayer = overPlayerEntity move 
movePickup = overPickupEntity move 

Мы также можем обернуть это в класс типа, чтобы сделать его проще написать универсальный код, а также:

class HasEntity a where 
    overEntity :: (Entity -> Entity) -> a -> a 

instance HasEntity Player where overEntity = overPlayerEntity 
instance HasEntity Pickup where overEntity = overPickupEntity 

Это позволяет такие вещи, как:

move' :: HasEntity a => a -> a 
move' = overEntity move 

, который работает с обоими Player с и Pickup s. Это устраняет необходимость в специализированной версии функций, таких как move, и в то же время нам нужно только написать шаблон доступа .

Кстати, этот способ делать вещи приближается к технике «линзы», упомянутой в конце ответов @ duplode и @Paul Johnson. Это по существу две (очень) специализированные линзы. Если мы добавим класс HasEntity, он даст нам то, что можно назвать «классным объективом» (это своего рода терминология, используемая в библиотеке lens). Вам не нужно беспокоиться о том, что означает или подразумевает концепция общей линзы, но это может дать вам точку входа, чтобы узнать о линзах в будущем.

Смежные вопросы