Почему явные для всех кванторов, необходимые для типов ранга-n?

Когда я объявляю этот новый тип:

newtype ListScott a = 
  ListScott { 
  unconsScott :: (a -> ListScott a -> r) -> r -> r 
}

который будет определять гипотетический тип ранга-2 ListScott :: ((a -> ListScott a -> r) -> r -> r) -> ListScott a, компилятор жалуется, что r не входит в область видимости. Не очевидно ли из сигнатуры типа, что я хочу передать полиморфную функцию первого класса на ListScott?

Зачем нужен явный квантификатор типа для r для таких случаев?

Я не теоретик типа и, вероятно, что-то пропустил...

Ответ 1

Это вопрос программирования языка программирования. Это можно сделать так, как вы предлагаете, но я утверждаю, что это плохая идея.

Не очевидно ли из сигнатуры типа, что я хочу передать полиморфную функцию первого класса в ListScott?

Я не думаю, что мы можем, очевидно, многое сказать об этом определении.

Универсальный или экзистенциальный? Конфликт с обозначением GADT

Здесь мы можем написать с расширением GADTs:

data ListScott a where
  ListScott :: { unconsScott :: (a -> ListScott a -> r) -> r -> r } -> ListScott a

Здесь r экзистенциально количественно определяется в поле unconsScott, поэтому конструктор имеет первый тип ниже:

ListScott :: forall a r. ((a -> ListScott a -> r) -> r -> r) -> ListScott a
-- as opposed to
ListScott :: forall a. (forall r. (a -> ListScott a -> r) -> r -> r) -> ListScott a

Вывод отключает обнаружение ошибок

Что делать, если r вместо этого означает параметр ListScott, но мы просто забыли его добавить? Я считаю, что это достаточно вероятная ошибка, потому что и гипотетические ListScott r a, и ListScott a могут служить в качестве представлений списков в некотором роде. Тогда вывод связующих будет приводить к принятию ошибочного определения типа, а ошибки сообщаются в другом месте, как только тип будет использоваться (надеюсь, не слишком далеко, но это все равно будет хуже, чем ошибка в самом определении).

Explicitness также предотвращает опечатки, когда конструктор типа получает туманность как переменную типа:

newtype T = T { unT :: maybe int }
-- unlikely to intentionally mean "forall maybe int. maybe int"

Недостаточно контекста в объявлении типа, чтобы уверенно догадываться о значении переменных, поэтому мы должны принудительно привязывать переменные должным образом.

читаемость

Рассмотрим запись функций:

data R a = R
  { f :: (r -> r) -> r -> r
  , g :: r -> r
  }

data R r a = R
  { f :: (r -> r) -> r -> r
  , g :: r -> r
  }

Мы должны посмотреть слева от =, чтобы определить, существует ли там r, а если нет, мы должны мысленно вставлять связующие в каждом поле. Я считаю, что первая версия трудно читать, потому что переменная r в двух полях фактически не будет находиться под одним и тем же переплетчиком, но она, безусловно, выглядит иначе.

Сравнение с аналогичными конструкциями

Обратите внимание, что что-то похожее на то, что вы предложили, происходит с типами классов, которое можно рассматривать как своего рода запись:

class Functor f where
  fmap :: (a -> b) -> f a -> f b

Большинство аргументов выше применимы также, и поэтому я бы предпочел написать этот класс как:

class Functor f where
  fmap :: forall a b. (a -> b) -> f a -> f b

Аналогичное утверждение можно было бы сказать о локальных аннотациях типа. Однако подписи верхнего уровня различны:

id :: a -> a

Это однозначно означает id :: forall a. a -> a, потому что нет другого уровня, где a может быть привязан.

Ответ 2

Дело в том, что конструктор не будет иметь тип ранга-1 (да, один), который вы упомянули: (кванторы добавлены для ясности)

ListScott1 :: forall a r. ((a -> ListScott a -> r) -> r -> r) -> ListScott a

но следующий тип ранга-2

ListScott2 :: forall a. (forall r. (a -> ListScott a -> r) -> r -> r) -> ListScott a

Итак, rank-2 действительно участвует в проверке типов вашей программы.

Обратите внимание, что если f :: (Bool -> ListScott Bool -> Char) -> Char -> Char, то первый конструктор выше сделал бы ListScott1 f :: ListScott Bool хорошо типизированным, но это не то, что мы хотим. Действительно, используя второй конструктор, ListScott2 f непечатаем.

Действительно, для корректного ввода ListScott2 f :: ListScott Bool нам нужен полиморфный f, имеющий тип f :: forall r. (Bool -> ListScott Bool -> r) -> r -> r.