Как указать параметры типа для At (типа карты) из Lens in Haskell подпись типа? - PullRequest
0 голосов
/ 19 октября 2018

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

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)
   |                            ^^^^^^^^^^^^^^^^

Ответы [ 2 ]

0 голосов
/ 21 октября 2018

Я пытался решить эту проблему с помощью сигнатур модулей и модулей mixin .

Сначала я объявил следующую подпись "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 для типов ключей, даже если - как в примере - реализация не использует его.Но вам нужно, чтобы позже можно было «заполнять» контейнеры на основе хеша, не изменяя подпись или код, который ее импортирует.

0 голосов
/ 19 октября 2018

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 с.

Вы можете попытаться поместить это ограничение в 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 mappish.Если я хочу создать a 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 не успокоится.)

...