Майкл уже дает хорошее объяснение в своей статье в блоге, но я попытаюсь проиллюстрировать его примером (надуманным, но относительно небольшим).
Нам понадобятся следующие расширения:
{-# LANGUAGE FlexibleInstances, TypeFamilies #-}
Давайте определим класс, который проще, чем CanFilter
, только с одним параметром. Я определяю два экземпляра класса, потому что я хочу, чтобы продемонстрировать разницу в поведении между двумя экземплярами:
class Twice1 f where
twice1 :: f -> f
class Twice2 f where
twice2 :: f -> f
Теперь давайте определим экземпляр для каждого класса. Для Twice1
мы фиксируем переменные типа одинаковыми напрямую, а для Twice2
мы позволяем им быть разными, но добавляем ограничение равенства.
instance Twice1 (a -> a) where
twice1 f = f . f
instance (a ~ b) => Twice2 (a -> b) where
twice2 f = f . f
Для того, чтобы показать разницу, давайте определим другой перегруженной функции, как это:
class Example a where
transform :: Int -> a
instance Example Int where
transform n = n + 1
instance Example Char where
transform _ = 'x'
Сейчас мы находимся в точке, где мы можем увидеть разницу. После того, как мы определим
apply1 x = twice1 transform x
apply2 x = twice2 transform x
и спросить GHC для предполагаемых типов, мы получаем, что
apply1 :: (Example a, Twice1 (Int -> a)) => Int -> a
apply2 :: Int -> Int
Почему? Ну, экземпляр для Twice1
срабатывает только тогда, когда источник и целевой тип функции совпадают. Для transform
и данного контекста мы этого не знаем. GHC будет применять экземпляр только после того, как правая сторона будет соответствовать, поэтому мы остаемся с неразрешенным контекстом. Если мы попробуем сказать apply1 0
, появится ошибка типа, согласно которой все еще недостаточно информации для разрешения перегрузки. Мы должны явно указать тип результата, который должен быть Int
в этом случае, чтобы пройти.
Однако в Twice2
экземпляр предназначен для любого типа функции. GHC немедленно ее устранит (GHC никогда не отступает, поэтому, если экземпляр явно соответствует, он всегда выбирается), а затем попытайтесь установить предварительные условия: в этом случае ограничение равенства, которое затем заставляет тип результата быть Int
и позволяет нам для устранения ограничения Example
. Мы можем сказать apply2 0
без дополнительных аннотаций типа.
Таким образом, это довольно тонкая точка зрения о разрешении экземпляров GHC, и ограничение равенства здесь помогает проверке типов GHC таким образом, чтобы пользовательский запрос требовал меньше аннотаций типа.
Спасибо. Я подозревал, что это что-то вроде этого, но ваш упрощенный пример сделал многое более ясным. – Clinton