Фантомный тип » а » — это переменная жесткого типа

#haskell

Вопрос:

 {-# LANGUAGE EmptyDataDecls #-}
module Main where

main :: IO ()
main = pure ()

data P a = P Int 
data D1
data D2

class C a where
instance C D1 where
instance C D2 where

mkP_D1 :: Int -> P D1
mkP_D1 = P

mkP_D2 :: Int -> P D2
mkP_D2 = P

ps :: C a => [P a]
ps = [mkP_D1 1, mkP_D2 2] 

qs :: Num a => [a]
qs = [1, 2]
 

выдает ошибку компилятора

  • Не удалось сопоставить тип » a » с » D1 «» a » — это жесткая переменная типа, связанная сигнатурой типа для: ps :: для всех a. C a => [P a]

    Ожидаемый тип: P a Фактический тип: P D1

  • В выражении: mkP_D1 1 В выражении: [mkP_D1 1, mkP_D2 2] В уравнении для «ps»: ps = [mkP_D1 1, mkP_D2 2]
  • Соответствующие привязки включают ps :: [P a]

    строка 22: ps = [mkP_D1 1, mkP_D2 2]

Почему это qs работает нормально, но все же ps не работает?

Комментарии:

1. В qs обоих элементах списка есть один и тот же тип a , в то время как в ps они разных типов P D1 и P D2 . Эквивалентная нерабочая версия qs будет qs = [ (1 :: Int), (2 :: Double) ]

2. Но в ps ограничении выполняется, так почему же компилятор жалуется?

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

4. Возможно, вы ищете список экзистенциально количественных значений, каждое из которых имеет свое собственное ограничение? Затем измените тип на ps :: [(forall a. C a => P a)] , это должно сработать.

5. По-видимому, это то, что я ищу. Компилятор предложил мне использовать расширение RankNTypes , но затем пожаловался * Незаконный полиморфный тип: forall a. C a => P a GHC еще не поддерживает импредикативный полиморфизм * В сигнатуре типа: ps :: [(forall a. C a => P a)]

Ответ №1:

Я постараюсь дать некоторую интуицию, немного упростив ситуацию.

ps :: C a => [P a] является договором между реализацией ps и ее пользователем. В соответствии с этим,

  • пользователь может выбрать любой a
  • пользователь должен доказать, что C a имеет
  • затем реализация должна предоставить значение типа [P a] .

Например, пользователь может выбрать a ~ D1 . Затем экземпляр используется для доказательства C D1 , чтобы договор был выполнен. В этом случае реализация должна предоставить значение типа [P D1] .

Ваша реализация такова [mkP_D1 1, mkP_D2 2] . В приведенном выше случае значение mkP_D1 1 следует за контрактом, поскольку оно равно a P D1 . Однако mkP_D2 2 не является P D1 (но является P D2 ), нарушающим контракт.

Поскольку существует экземпляр, в котором пользователь выполняет контракт, но реализация не выполняет, код отклоняется проверкой типов.

Более педантично, проблема здесь в том, что a выбирается пользователем, а не реализация. Если реализация попытается выбрать какой-то определенный тип a , есть вероятность, что пользователь выберет что-то другое, и компилятор в этом случае будет справедливо жаловаться.

Почему это прекрасно qs работает?

 qs :: Num a => [a]
qs = [1, 2]
 

Здесь пользователь выбирает a , но qs не делает этого. Действительно, 1 имеет любой (числовой) тип a , и это так 2 .

Сравните это с ps тем , который пытается выбрать a . Кстати, ps не только пытается выбрать a быть D1 (что уже вызовет ошибку «жесткой переменной»), но и пытается выбрать a быть D2 (что делает ситуацию еще хуже).

Возможной рабочей реализацией является добавление метода в класс:

 class C a where
  mkP :: Int -> P a

instance C D1 where
  mkP = P

instance C D2 where
  mkP = P

ps :: C a => [P a]
ps = [mkP 1, mkP 2] 
 

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

Комментарии:

1. Ага, так это та же проблема для qs = [1::Int]

2. @LoHaBuyshan Да, именно так.

Ответ №2:

Все элементы списка должны иметь один и тот же тип. qs работает, потому что все его элементы имеют один и тот же тип a . ps не работает, потому что его первый элемент имеет тип P D1 , а второй элемент имеет тип P D2 . Разных типов. Ничего не поделаешь.

Из комментариев стало очевидно, что на самом деле вы ищете, чтобы каждое значение имело другое Dx значение и имело свое собственное ограничение C . Чтобы достичь этого, заверните его в GADT:

 data PP where
  PP :: forall a. C a => P a -> PP

ps :: [PP]
ps = [PP (mkP_D1 1), PP (mkP_D2 2)]
 

Здесь конструктор PP упаковывает в него не только значение P a , но также тип a и соответствующее ему ограничение C a .

В: Оооо, но это так неудобно! Я должен обернуть каждую ценность в PP ! Неужели я не могу обойтись без этого?

Короткий ответ — нет, вы не можете. Как указывалось ранее, все элементы списка должны иметь один и тот же тип, точку. И если вы хотите поместить туда разные типы, вам нужно как-то обернуть их различия, чтобы они больше не были видны.