Понимание псевдонима типа ранга 2 с ограничением класса
У меня есть код, который часто использует функции, которые выглядят как
foo :: (MyMonad m) => MyType a -> MyOtherType a -> ListT m a
Чтобы попытаться сократить это, я написал следующий псевдоним типа:
type FooT m a = (MyMonad m) => ListT m a
GHC попросил меня включить Rank2Types (или RankNTypes), но не жаловался, когда я использовал псевдоним, чтобы сократить мой код до
foo :: MyType a -> MyOtherType a -> FooT m a
В отличие от этого, когда я написал псевдоним другого типа
type Bar a b = (Something a, SomethingElse b) => NotAsBar a b
и использовал его в отрицательном положении
bar :: Bar a b -> InsertTypeHere
GHC громко закричал на меня за то, что ошибался.
Я думаю, что у меня есть идея, что происходит, но я уверен, что смогу лучше понять ваши объяснения, поэтому у меня есть два вопроса:
- Каковы типичные псевдонимы типов/что они на самом деле означают?
- Есть ли способ получить терпение в обоих случаях?
Ответы
Ответ 1
В сигнатуре типа есть по существу три части:
- объявления переменных (они обычно неявные)
- переменные ограничения
- подпись подписи типа
Эти три элемента по существу складываются. Типовые переменные должны быть объявлены до того, как их можно будет использовать либо в ограничениях, либо в других местах, а также в ограничениях ограничений класса по всем видам использования в заголовке подписи типа.
Мы можем переписать ваш тип foo
, чтобы переменные были явно объявлены:
foo :: forall m a. (MyMonad m) => MyType a -> MyOtherType a -> ListT m a
Объявления переменных вводятся ключевым словом forall
и распространяются на .
. Если вы не представите их явно, GHC автоматически расширит их на верхнем уровне объявления. Ниже следуют ограничения, вплоть до =>
. Остальное - это подпись подписи типа.
Посмотрите, что произойдет, когда мы попытаемся объединиться в определении type FooT
:
foo :: forall m a. MyType a -> MyOtherType a -> ( (MyMonad m) => ListT m a )
Тип переменной m
создается на верхнем уровне foo
, но ваш псевдоним типа добавляет ограничение только в конечном значении! Существует два подхода к его исправлению. Вы можете:
- переместите forall до конца, поэтому
m
появится позже
- или переместите ограничение класса в начало
Перемещение ограничения в верхнюю часть выглядит как
foo :: forall m a. MyMonad m => MyType a -> MyOtherType a -> ListT m a
Предложение GHC о включении RankNTypes
делает первый (вроде, там что-то еще не хватает), в результате чего:
foo :: forall a. MyType a -> MyOtherType a -> ( forall m. (MyMonad m) => ListT m a )
Это работает, потому что m
больше нигде не появляется и справа от стрелки, поэтому эти два означают, по сути, одно и то же.
Сравните с bar
bar :: (forall a b. (Something a, SomethingElse b) => NotAsBar a b) -> InsertTypeHere
С псевдонимом типа в отрицательном положении тип более высокого ранга имеет другое значение. Теперь первый аргумент bar
должен быть полиморфным в a
и b
с соответствующими ограничениями. Это отличается от обычного значения, когда bar
вызывающий выбирает способ создания этих переменных типа. Это не
По всей вероятности, лучший подход - включить расширение ConstraintKinds
, которое позволяет создавать псевдонимы типов для ограничений.
type BarConstraint a b = (Something a, SomethingElse b)
bar :: BarConstraint a b => NotAsBar a b -> InsertTypeHere
Это не совсем так, как вы надеялись, но намного лучше, чем писать длинные ограничения каждый раз.
Альтернативой было бы изменить псевдоним типа в GADT, но у него есть несколько других последствий, которые вы, возможно, не захотите вводить. Если вы просто надеетесь получить более короткий код, я думаю, что ConstraintKinds
является лучшим вариант.
Ответ 2
Вы можете думать о ограничениях типа класса по существу как неявные параметры - например, о
Foo a => b
как
FooDict a -> b
где FooDict a
- словарь методов, определенных в классе Foo
. Например, EqDict
будет следующей записью:
data EqDict a = EqDict { equal :: a -> a -> Bool, notEqual :: a -> a -> Bool }
Различия заключаются в том, что для каждого типа может быть только одно значение каждого словаря (обобщайте соответственно для MPTC), а GHC заполняет его значение для вас.
С учетом этого мы можем вернуться к вашим подписям.
type FooT m a = (MyMonad m) => ListT m a
foo :: MyType a -> MyOtherType a -> FooT m a
расширяется до
foo :: MyType a -> MyOtherType a -> (MyMonad m => ListT m a)
с использованием интерпретации словаря
foo :: MyType a -> MyOtherType a -> MyMonadDict m -> ListT m a
что эквивалентно переупорядочению аргументов
foo :: MyMonadDict m -> MyType a -> MyOtherType a -> ListT m a
что эквивалентно обратному преобразованию словаря на
foo :: (MyMonad m) => MyType a -> MyOtherType a -> ListT m a
что вы искали.
Однако в другом примере все не так.
type Bar a b = (Something a, SomethingElse b) => NotAsBar a b
bar :: Bar a b -> InsertTypeHere
расширяется до
bar :: ((Something a, SomethingElse b) => NotAsBar a b) -> InsertTypeHere
Эти переменные все еще квантуются на верхнем уровне (т.е.
bar :: forall a b. ((Something a, SomethingElse b) => NotAsBar a b) -> InsertTypeHere
), так как вы указали их явно в подписи bar
, но когда мы делаем преобразование словаря
bar :: (SomethingDict a -> SomethingElseDict b -> NotAsBar a b) -> InsertTypeHere
мы видим, что это не эквивалентно
bar :: SomethingDict a -> SomethingElseDict b -> NotAsBar a b -> InsertTypeHere
который приведет к тому, что вы хотите.
Очень сложно придумать реалистичные примеры, в которых ограничение типа стекол используется в другом месте, чем его точка количественного определения - я никогда не видел его на практике - так что здесь нереально, чтобы показать, что это то, что происходит:
sillyEq :: forall a. ((Eq a => Bool) -> Bool) -> a -> a -> Bool
sillyEq f x y = f (x == y)
Контраст с тем, что произойдет, если мы будем использовать try для использования ==
, если мы не передаем аргумент f
:
sillyEq' :: forall a. ((Eq a => Bool) -> Bool) -> a -> a -> Bool
sillyEq' f x y = f (x == y) || x == y
мы получаем экземпляр No для ошибки Eq a
.
(x == y)
в sillyEq
получает свой Eq
dict от f
; его словарная форма:
sillyEq :: forall a. ((EqDict a -> Bool) -> Bool) -> a -> a -> Bool
sillyEq f x y = f (\eqdict -> equal eqdict x y)
Отступив немного назад, я думаю, что то, как вы здесь терпеливы, будет болезненным - я думаю, вы хотите просто использовать что-то, чтобы количественно определить его контекст, где его контекст определяется как "сигнатура функции, где Это использовано". У этого понятия нет простой семантики. Вы должны думать о bar
как о функции на множествах: она принимает в качестве аргументов два набора и возвращает другое. Я не верю, что будет такая функция для использования, которое вы пытаетесь достичь.
Что касается сокращающихся контекстов, вы можете использовать расширение ConstraintKinds
, которое позволяет создавать синонимы ограничений, поэтому, по крайней мере, вы могли бы сказать:
type Bars a = (Something a, SomethingElse a)
чтобы получить
bar :: Bars a => Bar a b -> InsertTypeHere
Но то, что вы хотите, по-прежнему возможно, - ваши имена недостаточно для описания. Вы можете захотеть изучить Экзистенциальное квантификация и Универсальная количественная оценка, которые являются двумя способами абстрагирования по переменным типа.
Мораль истории: помните, что =>
похож на ->
, за исключением того, что эти аргументы автоматически заполняются компилятором и убедитесь, что вы пытаетесь определить типы с определенными математическими значениями.