Понимание псевдонима типа ранга 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

Но то, что вы хотите, по-прежнему возможно, - ваши имена недостаточно для описания. Вы можете захотеть изучить Экзистенциальное квантификация и Универсальная количественная оценка, которые являются двумя способами абстрагирования по переменным типа.

Мораль истории: помните, что => похож на ->, за исключением того, что эти аргументы автоматически заполняются компилятором и убедитесь, что вы пытаетесь определить типы с определенными математическими значениями.