Перекрытие Haskell/Incoherent экземпляры

Я знаю, что это код немного глупый, но может ли кто-нибудь объяснить, почему этот isList [42] возвращает True, тогда как isList2 [42] печатает False, и как это предотвратить? Я хотел бы лучше понять некоторые из более неясных расширений типа GHC, и я подумал, что это будет интересный пример для выяснения.

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE IncoherentInstances #-}

class IsList a where
  isList :: a -> Bool

instance IsList a where
  isList x = False

instance IsList [a] where
  isList x = True

isList2 = isList

main = 
  print (isList 42) >> 
  print (isList2 42) >> 
  print (isList [42]) >> 
  print (isList2 [42]) 

Ответ 1

Это действительно довольно просто. Позвольте спросить GHCi, что тип isList2:

∀x. x ⊢ :t isList2
isList2 :: a -> Bool

Это не соответствует экземпляру [a] (даже если он может, через унификацию), но он сразу соответствует экземпляру a. Поэтому GHC выбирает экземпляр a, поэтому isList2 возвращает False.

Это то, что означает IncoherentInstances. На самом деле, это довольно хорошая демонстрация.


Весело, если вы просто отключите IncoherentInstances, мы получим точно противоположный эффект, и GHCi теперь говорит следующее:

∀x. x ⊢ :t isList2
isList2 :: [Integer] -> Bool

Это происходит потому, что isList2 - это привязка верхнего уровня, не определенная с использованием синтаксиса функций, и, таким образом, подчиняется Ограниченному ограничению мономорфизма. Таким образом, он становится специализированным для экземпляра, с которым он фактически использовался.

Добавив NoMonomorphismRestriction, а также отключив IncoherentInstances, мы получим это вместо:

∀x. x ⊢ :t isList2
isList2 :: IsList a => a -> Bool
∀x. x ⊢ isList2 'a'
False
∀x. x ⊢ isList2 "a"
True
∀x. x ⊢ isList2 undefined

<interactive>:19:1:
    Overlapping instances for IsList a0 arising from a use of `isList2'

Какое ожидаемое совпадающее поведение с выбранным экземпляром основано на использовании и жалобах, если выбор неоднозначен.


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

Первый вариант - предоставить isList2 подпись типа, которая не позволяет IncoherentInstances выбирать экземпляр слишком рано.

isList2 :: (IsList a) => a -> Bool
isList2 = isList

Вам, вероятно, придется делать то же самое в другом месте isList упоминается (даже косвенно) без применения к аргументу.

Второй вариант состоит в том, чтобы устранить неоднозначность числовых литералов и отключить IncoherentInstances.

main = 
  print (isList (42 :: Integer)) >> 
  print (isList2 (42 :: Integer)) >> 
  print (isList [42]) >> 
  print (isList2 [42]) 

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

Ответ 2

Следующий код делает трюк без требования IncoherentInstances:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverlappingInstances #-}

class IsList a where
  isList :: a -> Bool

instance IsList a where
  isList x = False

instance IsList [a] where
  isList x = True

isList2 :: (IsList a) => a -> Bool
isList2 = isList

main = do
  print (isList (42 :: Int))
  print (isList [42 :: Int])
  print (isList2 (42 :: Int))
  print (isList2 [42 :: Int])

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