Подтвердить что ты не робот

Почему этот код использует UndecidableInstances, а затем генерирует бесконечный цикл выполнения?

При написании кода с помощью UndecidableInstances раньше я столкнулся с чем-то, что я нашел очень странным. Мне удалось непреднамеренно создать некоторый код, который typechecks, когда я полагал, что это не должно:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}

data Foo = Foo

class ConvertFoo a b where
  convertFoo :: a -> b

instance (ConvertFoo a Foo, ConvertFoo Foo b) => ConvertFoo a b where
  convertFoo = convertFoo . (convertFoo :: a -> Foo)

evil :: Int -> String
evil = convertFoo

В частности, функция convertFoo typechecks при задании любого ввода для получения любого выхода, что продемонстрировано функцией evil. Сначала я подумал, что, возможно, мне удалось случайно реализовать unsafeCoerce, но это не совсем так: на самом деле попытка вызвать мою функцию convertFoo (например, делая что-то вроде evil 3) просто переходит в бесконечный цикл.

Я понимаю, что происходит в смутном смысле. Мое понимание проблемы заключается в следующем:

  • Экземпляр convertFoo, который я определил, работает на любом входе и выходе, a и b, ограниченном только двумя дополнительными ограничениями, что функции преобразования должны существовать от a -> Foo и Foo -> b.
  • Как бы то ни было, это определение может соответствовать любым типам ввода и вывода, поэтому кажется, что вызов convertFoo :: a -> Foo сам выбирает само определение convertFoo, так как его единственный доступный в любом случае.
  • Так как convertFoo вызывает себя бесконечно, функция переходит в бесконечный цикл, который никогда не заканчивается.
  • Так как convertFoo никогда не заканчивается, тип этого определения нижний, поэтому технически ни один из типов не нарушается, а тип typechecks.

Теперь, даже если вышеуказанное понимание верное, я все еще запутался в том, почему вся программа имеет тип. В частности, я ожидал бы, что ограничения ConvertFoo a Foo и ConvertFoo Foo b будут терпеть неудачу, учитывая, что таких экземпляров не существует.

Я понимаю (по крайней мере, пушисто), что ограничения не имеют значения при выборе экземпляра - экземпляр выбирается на основе только главы экземпляра, тогда ограничения проверяются, поэтому я вижу, что эти ограничения могут быть решены просто из-за моего ConvertFoo a b экземпляр, который примерно такой же разрешительный, как может быть. Тем не менее, для этого потребуется, чтобы один и тот же набор ограничений был разрешен, что, по-моему, поставит typechecker в бесконечный цикл, заставляя GHC либо зависать при компиляции, либо давать мне ошибку (последняя из которых я видел раньше).

Ясно, однако, что typechecker не работает, потому что он с радостью оканчивается и с радостью компилирует мой код. Зачем? Как контекст экземпляра удовлетворен в этом конкретном примере? Почему это не дает мне ошибку типа или создает цикл проверки типов?

4b9b3361

Ответ 1

Я искренне согласен с тем, что это большой вопрос. Это говорит о том, как наши интуиции о типах стилей отличаются от реальности.

Типичный сюрприз

Чтобы узнать, что здесь происходит, собираемся повысить ставки на для evil:

data X

class Convert a b where
  convert :: a -> b

instance (Convert a X, Convert X b) => Convert a b where
  convert = convert . (convert :: a -> X)

evil :: a -> b
evil = convert

Очевидно, что экземпляр Covert a b выбирается, поскольку существует только один экземпляр этого класса. Контролер типа думает что-то вроде это:

  • Convert a X истинно, если...
    • Convert a X истинно [истинно по предположению]
    • и Convert X X истинно
      • Convert X X истинно, если...
        • Convert X X истинно [истинно по предположению]
        • Convert X X истинно [истинно по предположению]
  • Convert X b истинно, если...
    • Convert X X истинно [true сверху]
    • и Convert X b истинно [истинно по предположению]

Тест-драйв удивил нас. Мы не ожидаем, что Convert X X будет правда, поскольку мы не определили ничего подобного. Но (Convert X X, Convert X X) => Convert X X - это своего рода тавтология: это автоматически true, и это верно независимо от того, какие методы определены в классе.

Это может не соответствовать нашей ментальной модели классов. Мы ожидаем, что компилятор, чтобы gawk в этот момент и жаловаться на то, как Convert X X не может быть истинным, потому что мы не определили для него никакого экземпляра. Мы ожидаем компилятор стоит на Convert X X, чтобы искать другое место идти туда, где Convert X X истинно, и отказаться, потому что там нет другого места, где это правда. Но компилятор способен рекурсию! Перезапустите, зацикливайтесь и завершите завершение.

Мы благословляли typechecker этой возможностью, и мы сделали это с UndecidableInstances. Когда в документации указано, что это возможно отправить компилятор в цикл, легко предположить и мы предположили, что плохие петли всегда бесконечные. Но здесь мы продемонстрировали цикл, даже более смертоносный, цикл, который заканчивается - за исключением удивительного способа.

(Это еще более ярко демонстрируется в комментарии Дэниела:

class Loop a where
  loop :: a

instance Loop a => Loop a where
  loop = loop

.)

Опасности неразрешимости

Это точная ситуация, когда UndecidableInstances позволяет. Если мы отключаем это расширение и поворачиваем FlexibleContexts на (безвредное расширение, которое является просто синтаксическим по своей природе), мы получаем предупреждение о нарушении одного из Paterson условия:

...
Constraint is no smaller than the instance head
  in the constraint: Convert a X
(Use UndecidableInstances to permit this)
In the instance declaration for ‘Convert a b’

...
Constraint is no smaller than the instance head
  in the constraint: Convert X b
(Use UndecidableInstances to permit this)
In the instance declaration for ‘Convert a b’

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

Снижение веса

Что касается того, почему программа во время выполнения бесконечных циклов: есть скука ответ, где evil :: a -> b не может, но бесконечный цикл или бросать исключение или вообще снизу, потому что мы доверяем Haskell typechecker и нет значения, которое может засечь a -> b за исключением снизу.

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

convertXX :: X -> X
convertXX = convertXX . convertXX

Мы также можем развернуть определение экземпляра Convert A B.

convertAB :: A -> B
convertAB =
  convertXB . convertAX
  where
    convertAX = convertXX . convertAX
    convertXX = convertXX . convertXX
    convertXB = convertXB . convertXX

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

Ответ 2

Вот как я мысленно обрабатываю эти случаи:

class ConvertFoo a b where convertFoo :: a -> b
instance (ConvertFoo a Foo, ConvertFoo Foo b) => ConvertFoo a b where
  convertFoo = ...

evil :: Int -> String
evil = convertFoo

Сначала начнем с вычисления множества требуемых экземпляров.

  • evil непосредственно требуется ConvertFoo Int String (1).
  • Тогда, (1) требует ConvertFoo Int Foo (2) и ConvertFoo Foo String (3).
  • Тогда (2) требуется ConvertFoo Int Foo (мы уже это подсчитали) и ConvertFoo Foo Foo (4).
  • Тогда (3) требуется ConvertFoo Foo Foo (подсчитано) и ConvertFoo Foo String (подсчитано).
  • Затем (4) требуется ConvertFoo Foo Foo (подсчитано) и ConvertFoo Foo Foo (подсчитано).

Следовательно, мы достигаем фиксированной точки, которая является конечным набором необходимых экземпляров. У компилятора нет проблем с вычислениями, установленными за конечное время: просто примените определения экземпляра, пока не потребуется больше ограничений.

Затем мы перейдем к предоставлению кода для этих экземпляров. Вот оно.

convertFoo_1 :: Int -> String
convertFoo_1 = convertFoo_3 . convertFoo_2
convertFoo_2 :: Int -> Foo
convertFoo_2 = convertFoo_4 . convertFoo_2
convertFoo_3 :: Foo -> String
convertFoo_3 = convertFoo_3 . convertFoo_4
convertFoo_4 :: Foo -> Foo
convertFoo_4 = convertFoo_4 . convertFoo_4

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