Что означает `~` (тильда) в контексте экземпляра, и почему в некоторых случаях необходимо разрешать перекрытие? - PullRequest
0 голосов
/ 01 сентября 2018

осложнение.

Рассмотрим следующий фрагмент:

class                        D u a     where printD :: u -> a -> String
instance                     D a a     where printD _ _ = "Same type instance."
instance {-# overlapping #-} D u (f x) where printD _ _ = "Instance with a type constructor."

И вот как это работает:

λ printD 1 'a'
...
...No instance for (D Integer Char)...
...

λ printD 1 1
"Same type instance."

λ printD [1] [1]
...
...Overlapping instances for D [Integer] [Integer]
...

λ printD [1] ['a']
"Instance with a type constructor."

Обратите внимание, что перекрывающиеся экземпляры не разрешаются, несмотря на прагму, предоставленную этому конец.

Решение.

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

class                        D' u a     where printD' :: u -> a -> String
instance (u ~ a) =>          D' u a     where printD' _ _ = "Same type instance."
instance {-# overlapping #-} D' u (f x) where printD' _ _ = "Instance with a type constructor."

Работает, как я ожидал, до:

λ printD' 1 'a'
...
...No instance for (Num Char)...
...

λ printD' 1 1
"Same type instance."

λ printD' [1] [1]
"Instance with a type constructor."

λ printD' [1] ['a']
"Instance with a type constructor."

Мои вопросы.

Мне трудно понять, что здесь происходит. Есть объяснение?

В частности, я могу задать два отдельных вопроса:

  1. Почему перекрытие не разрешено в первом фрагменте?
  2. Почему перекрытие разрешается во втором фрагменте?

Но, если проблемы связаны, возможно, одна единая теория могла бы лучше объяснить этот случай.

P.S. в отношении близкого / дублирующего голосования Мне известно, что ~ означает равенство типов, и я сознательно использую его для получения нужного мне поведения (в частности, printD' 1 'a' не соответствует) . Вряд ли это что-то объясняет, в частности, тот случай, который я представил, когда два способа определения равенства типов (~ и instance D a a) приводят к двум тонко различимым поведениям.


note Я тестировал приведенные выше фрагменты с ghc 8.4.3 и 8.6.0.20180810

1 Ответ

0 голосов
/ 01 сентября 2018

Первый: во время выбора экземпляра имеет значение только голова экземпляра: то, что слева от =>, не имеет значения. Таким образом, instance D a a предотвращает выбор, если они не равны; instance ... => D u a всегда можно выбрать.

Теперь, прагмы перекрытия вступают в игру только в том случае, если один экземпляр уже более "специфичен", чем другой. «Специфично» в данном случае означает «если существует замена переменных типа, которые могут создавать экземпляр экземпляра A для заголовка экземпляра B, то B является более специфичным, чем A». В

instance D a a
instance {-# OVERLAPPING #-} D u (f x)

ни один не является более конкретным, чем другие, поскольку нет замены a := ?, которая превращает D a a в D u (f x), и нет никакой замены u := ?; f := ?; x := x, которая превращает D u (f x) в D a a. Прагма {-# OVERLAPPING #-} ничего не делает (по крайней мере, относится к проблеме). Таким образом, при разрешении ограничения D [Integer] [Integer] компилятор находит оба экземпляра как кандидатов, не более конкретных, чем другие, и выдает ошибку.

В

instance (u ~ a) => D u a
instance {-# OVERLAPPING #-} D u (f x)

второй экземпляр более специфичен, чем первый, потому что первый экземпляр можно создать с помощью u := u; a := f x, чтобы перейти ко второму. Прагма теперь тянет на себя. При разрешении D [Integer] [Integer] оба экземпляра совпадают, первый с u := [Integer]; a := [Integer], а второй с u := [Integer]; f := []; x := Integer. Однако второй является более конкретным и OVERLAPPING, поэтому первый отбрасывается как кандидат, а второй используется. (Примечание: я думаю, что экземпляр first должен быть OVERLAPPABLE, а второй экземпляр не должен иметь прагмы. Таким образом, все будущие экземпляры неявно перекрывают экземпляр catch-all вместо необходимости аннотировать каждый один.)

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

Одним из способов визуализации происходящего является диаграмма Венна. С первой попытки instance D a a и instance D u (f x) образуют два набора, наборы пар типов, которым может соответствовать каждый. Эти наборы перекрываются, но существует множество пар типов только с совпадениями D a a и много пар только с D u (f x) совпадениями. Ни один из них не может быть назван более конкретным, поэтому прагма OVERLAPPING не удалась. Во второй попытке D u a фактически охватывает всю вселенную пар типов, а D u (f x) является ее подмножеством (читай: внутри). Теперь прагма OVERLAPPING работает. Мышление таким образом также показывает нам другой способ сделать эту работу, создав новый набор, который точно покрывает пересечение первой попытки.

instance D a a
instance D u (f x)
instance {-# OVERLAPPING #-} (f x) (f x)

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

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

См. Также руководство GHC .

...