-
QuantifiedConstraints -
- Подразумевает:
- Так как:
-
8.6.1
Разрешить ограничения для квантификации по типам.
Расширение QuantifiedConstraints вводит квантифицированные ограничения, которые предоставляют новый уровень выразительности в ограничениях. Например, рассмотрим
data Rose f a = Branch a (f (Rose f a))
instance (Eq a, ???) => Eq (Rose f a)
where
(Branch x1 c1) == (Branch x2 c2)
= x1==x2 && c1==c2
Из x1==x2 нам нужно Eq a, что нормально. Из c1==c2 нам нужно Eq (f (Rose f a)), что не нормально в Haskell сегодня; у нас нет способа решить такое ограничение.
QuantifiedConstraints позволяет нам записать это
instance (Eq a, forall b. (Eq b) => Eq (f b))
=> Eq (Rose f a)
where
(Branch x1 c1) == (Branch x2 c2)
= x1==x2 && c1==c2
Здесь квантифицированное ограничение forall b. (Eq b) => Eq (f b) ведет себя немного как локальное объявление экземпляра и делает экземпляр типизируемым.
Статья Quantified class constraints (Боту, Карахалияс, Шриверс, Оливейра, Уолдлер, Симпозиум по Haskell 2017) подробно описывает эту функцию с примерами, и поэтому является основным источником ссылок для этой функции.
6.10.4.1. Мотивация
Введение квантифицированных ограничений предоставляет две основные выгоды:
-
Во-первых, они позволяют завершать разрешение там, где это не было возможно раньше. Например, рассмотрим следующее объявление экземпляра для общего типа данных rose:
data Rose f x = Rose x (f (Rose f x)) instance (Eq a, forall b. Eq b => Eq (f b)) => Eq (Rose f a) where (Rose x1 rs1) == (Rose x2 rs2) = x1 == x2 && rs1 == rs2
Это расширение позволяет нам записывать ограничения вида
forall b. Eq b => Eq (f b), которые необходимы для решения ограниченияEq (f (Rose f x)), возникающего из второго использования метода(==). -
Во-вторых, квантифицированные ограничения позволяют более лаконично и точно задавать спецификации. В качестве примера рассмотрим класс типа MTL для трансформаторов монады:
class Trans t where lift :: Monad m => m a -> (t m) a
Разработчик знает, что трансформатор монады принимает монаду
mв новую монадуt m. Но это свойство не формально указано в вышеприведенном объявлении. Это упущение становится проблемой при определении композиции трансформаторов монады:newtype (t1 * t2) m a = C { runC :: t1 (t2 m) a } instance (Trans t1, Trans t2) => Trans (t1 * t2) where lift = C . lift . liftЦель здесь состоит в том, чтобы
liftот монадыmкt2 mи затемliftэто снова вt1 (t2 m). Однако, этот второйliftможет быть принят только тогда, когда(t2 m)является монадой, и нет способа установить, что это утверждение универсально верно.Квантифицированные ограничения позволяют сделать это свойство явным в объявлении класса
Trans.class (forall m. Monad m => Monad (t m)) => Trans t where lift :: Monad m => m a -> (t m) a
Эта идея очень старая; см. раздел 7 Derivable type classes.
6.10.4.2. Изменения в синтаксисе
Haskell 2010 определяет context (часть слева от => в типе) следующим образом:
context ::= class
| ( class1, ..., classn )
class ::= qtycls tyvar
| qtycls (tyvar atype1 ... atypen)
Мы расширяем class (предупреждение: это довольно запутанно названный нетерминальный символ) двумя дополнительными формами, а именно тем, что может появиться в объявлении экземпляра.
class ::= ...
| [context =>] qtycls inst
| [context =>] tyvar inst
Определение inst не изменяется по сравнению с отчетом Haskell (приблизительно, просто тип). Часть context => является необязательной. Это единственное синтаксическое изменение языка.
Примечания:
-
Там, где GHC допускает расширения в объявлениях экземпляров, мы допускаем точно такие же расширения для этой новой формы
class. В частности, сExplicitForAllиMultiParamTypeClassesсинтаксис становится:class ::= ... | [forall tyvars .] [context =>] qtycls inst1 ... instn | [forall tyvars .] [context =>] tyvar inst1 ... instnОбратите внимание, что явное
forallзачастую абсолютно необходимо. Рассмотрим пример дерева розы:instance (Eq a, forall b. Eq b => Eq (f b)) => Eq (Rose f a) where ...
Без
forall b, переменная типаbбудет квантифицирована по всему объявлению экземпляра, что не является желаемым. -
Одно из этих новых квантифицированных ограничений может появиться в любом месте, где может появиться любое другое ограничение, а не только в объявлениях экземпляров. В частности, оно может появиться в сигнатуре типа для связывания значений, конструктора данных или выражения. Например:
f :: (Eq a, forall b. Eq b => Eq (f b)) => Rose f a -> Rose f a -> Bool f t1 t2 = not (t1 == t2)
-
Форма с переменной типа в начале позволяет это:
instance (forall xx. c (Free c xx)) => Monad (Free c) where Free f >>= g = f gСм. резюме Айсленда Джека. Суть в том, что часть справа от
=>может быть заголовком переменной типа (cв данном случае), а не класса. Хотя она не должна быть одной из переменных, для которых используется forall.(Прим.: это выходит за рамки описанного в статье, но, кажется, не вносит никаких новых технических трудностей.)
6.10.4.3. Изменения в типизации
См. статью.
6.10.4.4. Суперклассы
Предположим, у нас есть:
f :: forall m. (forall a. Ord a => Ord (m a)) => m Int -> Bool f x = x == x
Из x==x нам нужно ограничение Eq (m Int), но контекст предоставляет только способ определить ограничения Ord (m a). Но из заданного ограничения forall a. Ord a => Ord (m a) мы получаем второе заданное ограничение forall a. Ord a => Eq (m a), и из него мы можем легко решить Eq (m Int). Этот процесс очень похож на работу с суперклассами: из ограничения Ord a мы получаем второе заданное ограничение Eq a.
Прим.: Это рассмотрение суперклассов выходит за рамки статьи, но конкретно желается пользователями.
6.10.4.5. Пересечение
Квантифицированные ограничения могут потенциально привести к перекрывающимся локальным аксиомам. Например, рассмотрим следующий пример:
class A a where {}
class B a where {}
class C a where {}
class (A a => C a) => D a where {}
class (B a => C a) => E a where {}
class C a => F a where {}
instance (B a, D a, E a) => F a where {}
При проверке типа объявления экземпляра для F a, необходимо проверить, что суперкласс C для F выполняется. Таким образом, мы пытаемся вывести ограничение C a в теории, содержащей:
- Аксиомы экземпляра:
(B a, D a, E a) => F a - Локальные аксиомы из контекста экземпляра:
B a,D aиE a - Закрытие отношения суперкласса над этими локальными аксиомами:
A a => C aиB a => C a
Однако, аксиомы A a => C a и B a => C a обе соответствуют нужному ограничению C a. Существует несколько возможных подходов к обработке этих перекрывающихся локальных аксиом:
-
Выбор первого. Мы можем просто выбрать первую подходящую аксиому, которую мы встретим. В приведенном выше примере это будет
A a => C a. Затем нам нужно будет вывестиA a, для которого у нас нет доступных соответствующих аксиом, что приведет к отклонению вышеприведенной программы.Но предположим, что мы внесли небольшое изменение в порядок контекста экземпляра, поставив
E aпередD a:instance (B a, E a, D a) => F a where {}Первая подходящая аксиома, которую мы встречаем при выводе
C a, этоB a => C a. У нас есть локальная аксиомаB aдоступна, поэтому теперь программа внезапно принимается. Такое поведение, где порядок контекста экземпляра определяет, принимается ли программа или нет, кажется довольно запутанным для разработчика. - Отклонение при сомнении. Альтернативный подход заключался бы в проверке перекрывающихся аксиом при решении ограничения. Когда обнаруживаются несколько соответствующих аксиом, мы отклоняем программу. Этот подход немного консервативен, поскольку он может отклонить работающие программы. Но он кажется более прозрачным для разработчика, которому может быть представлено четкое сообщение, объясняющее, почему программа отклонена.
-
Возврат. Наконец, можно ввести простую форму возврата. Мы просто выбираем первую подходящую аксиому, которую встречаем, и когда вывод не удается, мы возвращаемся назад и ищем другие аксиомы, которые могут соответствовать нужному ограничению.
Это, кажется, наиболее интуитивный и прозрачный подход для разработчика, которому больше не нужно беспокоиться о том, что его код может содержать перекрывающиеся аксиомы или о порядке его контекстов экземпляров. Но возврат будет применяться и к обычному выбору экземпляра (при наличии перекрывающихся экземпляров), поэтому это гораздо более широкомасштабное изменение со значительными последствиями для механизма вывода типов.
GHC принимает Отклонение при сомнении на данный момент. Мы можем увидеть, насколько это бывает неприятно на практике, и попробовать что-то более амбициозное, если необходимо.
6.10.4.6. Поиск экземпляра
В свете решения по перекрытию, поиск экземпляра работает следующим образом при попытке решить ограничение класса C t
- Сначала посмотрите, есть ли заданное неквантифицированное ограничение
C t. Если да, используйте его для решения ограничения. - Если нет, посмотрите на все доступные заданные квантифицированные ограничения; если ровно одно соответствует
C t, выберите его; если соответствует более одного, выведите ошибку. - Если ни одно квантифицированное ограничение не соответствует, выполните поиск по глобальным экземплярам, как описано в Объявления и разрешение экземпляров и Перекрывающиеся экземпляры.