At m
обеспечивает at :: Index m -> Lens' m (Maybe (IxValue m))
. Обратите внимание, что Lens' m _
означает, что m
— это конкретный тип, такой как Int
или Map ImageId Sprite
, а не конструктор типа, такой как Map
. Если вы хотите сказать, что m ImageId Sprite
похожа на карту, вам нужны эти 3 ограничения:
At (m ImageId Sprite)
: предоставляет at
для индексации и обновления.
Index (m ImageId Sprite) ~ ImageId
: для индексации m ImageId Sprite
используются ключи ImageId
.
IxValue (m ImageId Sprite) ~ Sprite
: значения в m ImageId Sprite
равны Sprite
s.
Вы можете попробовать поместить это ограничение в Game
(хотя это все равно неправильно):
data Game m e = Game {
initial :: e,
-- ...
sprites :: (At (m ImageId Sprite),
Index (m ImageId Sprite) ~ ImageId,
IxValue (m ImageId Sprite) ~ Sprite) =>
IO (m ImageId Sprite)
}
Обратите внимание, что я говорю m ImageId Sprite
миллион раз, но я не применяю m
к другим (или меньшим) параметрам. Это ключ к тому, что вам на самом деле не нужно абстрагироваться от m :: * -> * -> *
(таких вещей, как Map
). Вам нужно только абстрагироваться от m :: *
.
-- still wrong, though
type IsSpriteMap m = (At m, Index m ~ ImageId, IxValue m ~ Sprite)
data Game m e = Game {
initial :: e,
-- ...
sprites :: IsSpriteMap m => IO m
}
Это хорошо: если вы когда-нибудь сделаете специализированную карту для этой структуры данных, например
data SpriteMap
instance At SpriteMap
type instance Index SpriteMap = ImageId
type instance IxValue SpriteMap = IxValue
вы бы не смогли использовать его со слишком абстрактным Game
, но он идеально вписывается в менее абстрактный Game SpriteMap e
.
Однако это все равно неправильно, потому что ограничение находится не в том месте. Здесь вы сказали следующее: если у вас есть Game m e
, вы можете получить m
, если вы докажете, что m
соответствует карте. Если я хочу создать Game m e
, я не обязан доказывать, что m
вообще является картографическим. Если вы не понимаете, почему, представьте, если бы вы могли заменить =>
на ->
выше. Человек, который называет sprites
, передает доказательство того, что m
похоже на карту, но Game
не содержит доказательств.
Если вы хотите сохранить m
в качестве параметра для Game
, вы должны просто написать:
data Game m e = Game {
initial :: e,
-- ...
sprites :: IO m
}
И напишите каждую функцию, которая должна использовать m
в качестве карты, например:
doSomething :: IsSpriteMap m => Game m e -> IO ()
Или вы можете использовать экзистенциальную количественную оценку:
data Game e = forall m. IsSpriteMap m => Game {
initial :: e,
-- ...
sprites :: IO m
}
Чтобы построить Game e
, вы можете использовать что угодно типа IO m
для заполнения sprites
, пока IsSpriteMap m
. Когда вы используете Game e
в сопоставлении с образцом, сопоставление с образцом свяжет переменную (безымянного) типа (назовем ее m
), а затем даст вам IO m
и доказательство для IsSpriteMap m
.
doSomething :: Game e -> IO ()
doSomething Game{..} = do sprites' <- sprites
imageId <- _
let sprite = sprites'^.at imageId
_
Вы также можете оставить m
в качестве параметра для Game
, но сохранить контекст в конструкторе Game
. Тем не менее, я призываю вас просто использовать первый вариант размещения контекста для каждой функции, если только у вас нет причин не делать этого.
(Весь код в этом ответе выдает ошибки, связанные с языковыми расширениями. Продолжайте вставлять их в прагму {-# LANGUAGE <exts> #-}
вверху вашего файла, пока GHC не успокоится.)
person
HTNW
schedule
19.10.2018