Строгая позитивность

10

Из этой ссылки: Строгий позитив

Строгое условие позитивности исключает такие объявления, как

data Bad : Set where
 bad : (Bad → Bad) → Bad
         A      B       C
 -- A is in a negative position, B and C are OK

Почему А отрицательный? Также почему Б разрешено? Я понимаю, почему C допускается.

Pushpa
источник
1
Я не уверен, почему это называется «отрицательным», но он более известен по ошибке, которую он производит: переполнение стека :) Этот код может Aв конечном итоге привести к бесконечному расширению и взрыву стека (в языках на основе стека).
wvxvw
В этой части я понимаю, что вы можете писать произвольные вещи, и, следовательно, вычисления не будут прерываться. спасибо
Пушпа
1
Я думаю, что было бы хорошо упомянуть неразрывность в теле вашего вопроса. Я обновил свой ответ на основе вашего комментария.
Антон Трунов
@wvxvw Не обязательно, он может работать вечно, не разрушая стек, при условии, что компилятор реализует хвостовую рекурсию, например, мой пример в OCaml ниже не взрывает стек.
Антон Трунов
1
@AntonTrunov, конечно, это было больше словосочетанием по названию сайта, чем попыткой быть точным.
wvxvw

Ответы:

17

Сначала терминологическое объяснение: отрицательные и положительные позиции исходят из логики. Они об асимметрии в логических связок: в ведет себяразному от B . Похожая вещь происходит в теории категорий, где мы говоримконтравариантныйиковариантныйвместо отрицательного и положительного соответственно. В физике они говорят о величинах, которые ведут себя «ковариантно» и «тоже контравариантно». Так что это очень общее явление. Программист может думать о них как о «входе» и «выходе».AВAВ

Теперь на индуктивные типы данных.

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

В алгебре принято, что операция принимает конечное число аргументов, и в большинстве случаев она принимает ноль (постоянный), один (унарный) или два (двоичных) аргумента. Это удобно обобщить для конструкторов типов данных. Предположим c, это конструктор для типа данных T:

  • Если cэто константа, мы можем думать о ней как о функции unit -> T, или эквивалентно (empty -> T) -> T,
  • если cунарный, мы можем думать о нем как о функции T -> Tили, что то же самое (unit -> T) -> T,
  • если cбинарный, мы можем думать о нем как о функции T -> T -> T, или эквивалентно T * T -> T, или эквивалентно (bool -> T) -> T,
  • если бы мы хотели конструктор, cкоторый принимает семь аргументов, мы могли бы рассматривать его как функцию, (seven -> T) -> Tгде sevenесть некоторый ранее определенный тип с семью элементами.
  • у нас также может быть конструктор, cкоторый принимает бесконечно много аргументов, это будет функция (nat -> T) -> T.

Эти примеры показывают, что общая форма конструктора должна быть

c : (A -> T) -> T

куда мы звоним A на Арность о cи мы думаем, cкак конструктор , который принимает A-many аргументов типа Tдля получения элемента T.

Здесь есть кое-что очень важное: арность должна быть определена до того, как мы ее определим T, иначе мы не можем сказать, что должны делать конструкторы. Если кто-то пытается получить конструктор

broken: (T -> T) -> T

тогда вопрос "сколько аргументов broken нужно?" не имеет хорошего ответа. Вы можете попытаться ответить на него Tсловами «требуется множество аргументов», но это не сработает, поскольку Tеще не определено. Мы могли бы попытаться выбраться из кунундрума, используя причудливую теорию с фиксированной точкой, чтобы найти тип Tи инъективную функцию (T -> T) -> T, и добились бы успеха, но мы также нарушили бы принцип индукции на Tэтом пути. Так что это просто плохая идея попробовать такую ​​вещь.

Ради полноты позвольте мне объяснить всю историю. Нам нужно немного обобщить приведенную выше форму конструкторов. Иногда у нас есть операции или конструкторы, которые принимают параметры . Например, для скалярного умножения требуется скаляр и вектор v, чтобы получить вектор λ v . Это одинарная операция над векторами, параметризованная скаляром. Мы могли быλvλv смотреть скалярное умножение бесконечно много одинарных операций, по одному для каждого скаляра, но это раздражает. Итак, общая форма конструктора cдолжна позволять параметр некоторого типа B:

c : B * (A -> T) -> T

Действительно, многие конструкторы можно переписать таким образом, но не все, нам нужно еще один шаг, а именно мы должны позволить , Aчтобы зависеть от B:

c : (∑ (x : B), A x -> T) -> T

Это окончательная форма конструктора для индуктивного типа. Это также точно, что W-типы. Форма настолько общая, что нам нужен только один конструкторc ! Действительно, если у нас есть два из них

d' : (∑ (x : B'), A' x -> T) -> T
d'' : (∑ (x : B''), A'' x -> T) -> T

тогда мы можем объединить их в один

d : (∑ (x : B), A x -> T) -> T

где

B := B' + B''
A(inl x) := A' x
A(inr x) := A'' x

Кстати, если мы карри общего вида, мы видим, что это эквивалентно

c : ∏ (x : B), ((A x -> T) -> T)

что ближе к тому, что люди на самом деле записывают в виде помощников. Помощники по проверке позволяют нам записывать конструкторы удобным способом, но они эквивалентны общей форме выше (упражнение!).

Андрей Бауэр
источник
1
Спасибо еще раз, Андрей, после моего ланча, это будет труднее всего переварить. Приветствия.
Пушпа
9

Первое вхождение Badназывается «отрицательным», потому что оно представляет аргумент функции, т. Е. Находится слева от стрелки функции (см. « Рекурсивные типы бесплатно» от Филиппа Вадлера). Я предполагаю, что происхождение термина «отрицательная позиция» происходит от понятия контравариантности («контра» означает противоположное).

Не допускается, чтобы тип определялся в отрицательной позиции, потому что можно писать не завершающие программы, используя его, то есть сильная нормализация терпит неудачу в его присутствии (подробнее об этом ниже). Кстати, это и есть причина названия правила «строгая позитивность»: мы не допускаем отрицательных позиций.

Мы допускаем второе вхождение, Badпоскольку оно не вызывает не-завершение, и мы хотим использовать определяемый тип ( Bad) в некоторой точке рекурсивного типа данных ( перед последней стрелкой его конструктора).

Важно понимать, что следующее определение не нарушает строгое правило позитивности.

data Good : Set where
  good : Good → Good → Good

Правило применяется только к аргументам конструктора (которые Goodв данном случае оба ), а не к самому конструктору (см. Также « Сертифицированное программирование с зависимыми типами » Адама Члипалы ).

Еще один пример нарушения строгой позитивности:

data Strange : Set where
  strange : ((Bool → Strange) → (ℕ → Strange)) → Strange
                       ^^     ^
            this Strange is   ...this arrow
            to the left of... 

Вы также можете проверить этот ответ на отрицательные позиции.


Подробнее о недопустимости ... Страница, на которую вы ссылаетесь, содержит некоторые пояснения (наряду с примером на Хаскеле):

Не строго положительные объявления отклоняются, потому что можно написать не завершающую функцию, используя их. Чтобы увидеть, как можно написать определение цикла, используя тип данных Bad сверху, см. BadInHaskell .

Вот аналогичный пример в Ocaml, который показывает, как реализовать рекурсивное поведение без (!) Непосредственного использования рекурсии:

type boxed_fun =
  | Box of (boxed_fun -> boxed_fun)

(* (!) in Ocaml the 'let' construct does not permit recursion;
   one have to use the 'let rec' construct to bring 
   the name of the function under definition into scope
*)
let nonTerminating (bf:boxed_fun) : boxed_fun =
  match bf with
    Box f -> f bf

let loop = nonTerminating (Box nonTerminating)

В nonTerminatingфункции «распаковывается» функция от аргумента и яблок с оригиналом аргумента. Здесь важно то, что большинство систем типов не разрешают передавать функции самим себе, поэтому такой термин, как « f fпроверка типов», не будет проверяться, поскольку не существует типа, fудовлетворяющего проверке типов. Одной из причин, по которой были введены системы типов, является отключение самостоятельного применения (см. Здесь ).

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

Я хочу добавить, что бесконечные вычисления вносят несоответствия в логические системы. В случае Agda и Coq Falseиндуктивный тип данных не имеет конструкторов, поэтому вы никогда не сможете создать проверочный термин типа False. Но если бы разрешались не завершающиеся вычисления, это можно сделать, например, так (в Coq):

Fixpoint loop (n : nat) : False = loop n

Тогда loop 0будет проверка типов loop 0 : False, так что при переписке Карри-Говарда это будет означать, что мы доказали ложное предположение.

Upshot : строгое правило положительности для индуктивных определений предотвращает неразрывные вычисления, которые губительны для логики.

Антон Трунов
источник
Теперь я в замешательстве. Специально данные Хорошо: Укажите, где хорошо: Хорошо → Хорошо →. Мы попытаемся понять и вернуться через час /
Пушпа
Правило не относится к самому конструктору, только к его аргументам, то есть стрелки на верхнем уровне определения конструктора не имеют значения. Я также добавил еще один (косвенный) пример нарушения.
Антон Трунов