Как указать параметры типа для At (картоподобные типы) из Lens в подписи типа Haskell?

Я хотел бы ограничить тип ключа ImageId и тип значения Sprite, оставив без ограничений конкретный тип карты с помощью В классе типов. Это возможно? Кажется, я получаю какое-то несоответствие, и, основываясь на сигнатуре типа, я не вижу, как его решить. Мой пример:

data Game m e = Game {
  initial :: e,
  -- ...
  sprites :: (At m) => IO (m ImageId Sprite)
}

Моя ошибка:

    * Expected kind `* -> * -> *', but `m' has kind `*'
    * In the first argument of `IO', namely `(m ImageId Sprite)'
      In the type `(At m) => IO (m ImageId Sprite)'
      In the definition of data constructor `Game'
   |
64 |   sprites :: (At m) => IO (m ImageId Sprite)
   |                            ^^^^^^^^^^^^^^^^

person bbarker    schedule 19.10.2018    source источник


Ответы (2)


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 равны Sprites.

Вы можете попробовать поместить это ограничение в 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
comment
Отличный ответ. - person leftaroundabout; 19.10.2018
comment
Самый простой вариант — вообще не использовать параметр sprites :: Map ImageId IxValue. - person Benjamin Hodgson♦; 19.10.2018
comment
Это очень четкий ответ, и я многому научился. У меня есть один вопрос после того, как я немного поиграл с обоими предложениями: почему бы не предпочесть forall? Кажется, что это менее инвазивно, так как нужно изменить только один аспект кода, если эта часть API будет изменена снова (ну, надеюсь, только одна часть). - person bbarker; 19.10.2018
comment
@bbarker Есть некоторые вещи, которые вы можете сделать с одним, но не можете сделать с другим. Если вам нужны только те функциональные возможности, которые они разделяют, размещение контекстов в функциях является самым простым решением (меньше языковых расширений). - person HTNW; 20.10.2018

Я попытался решить эту проблему, используя подписи модулей и модули примесей.

Сначала я объявил следующую подпись "Mappy.hsig" в основной библиотеке:

{-# language KindSignatures #-}
{-# language RankNTypes #-}
signature Mappy where

import Control.Lens
import Data.Hashable

data Mappy :: * -> * -> *

at' :: (Eq i, Ord i, Hashable i) => i -> Lens' (Mappy i v) (Maybe v)

Я не мог использовать класс типов At напрямую из-за этого ограничения.

Затем я заставил код библиотеки импортировать абстрактную подпись вместо конкретного типа:

{-# language DeriveGeneric #-}
{-# language DeriveAnyClass #-}
module Game where

import Data.Hashable
import GHC.Generics
import Mappy (Mappy,at')

data ImageId = ImageId deriving (Eq,Ord,Generic,Hashable)

data Sprite = Sprite

data Game e = Game {
  initial :: e,
  sprites :: IO (Mappy ImageId Sprite)
}

Код в библиотеке не знает, каким будет конкретный тип Mappy, но он знает, что функция at' доступна, когда ключ удовлетворяет ограничениям. Обратите внимание, что Game не параметризован типом карты. Вместо этого вся библиотека становится неопределенной благодаря наличию подписи, которую должны заполнить позже пользователи библиотеки.

Во внутренней вспомогательной библиотеке ( или полностью отдельный пакет) я определил модуль реализации с тем же именем, что и сигнатура:

{-# language RankNTypes #-}
module Mappy where

import Data.Map.Strict
import Control.Lens
import Data.Hashable

type Mappy = Map 

at' :: (Eq i, Ord i, Hashable i) => i -> Lens' (Mappy i v) (Maybe v)
at' = at

Исполняемый файл зависит как от основной библиотеки, так и от библиотеки реализации. «Дыра» сигнатуры в основной библиотеке заполняется автоматически, потому что существует модуль реализации с таким же именем, и содержащиеся в нем объявления удовлетворяют сигнатуре.

module Main where

import Game
import qualified Data.Map

game :: Game () 
game = Game () (pure Data.Map.empty)

Одним из недостатков этого решения является то, что для типов ключей требуется экземпляр Hashable, даже если, как в примере, реализация его не использует. Но вам это нужно, чтобы разрешить «заполнение» контейнеров на основе хеша позже, без изменения подписи или кода, который ее импортирует.

person danidiaz    schedule 21.10.2018