shooting off your leg with class
Feb. 11th, 2013 02:09 pmЗахотелось мне недавно реализовать циклический сдвиг непустого вектора (списка с длиной в типе) на Идрисе. Пишу:
А компилятор мне отвечает, что он хоть и умный, но ленивый, и ему не очевидно, что n+1 это n+1, ну в смысле что результат применения функции сложения натурального числа n с константой 1 это следующее за n число.
"Can't unify Vect a (m + n) with Vect a (S n)
Specifically: Can't unify plus m with S"
Для случаев, когда код правильный, но компилятор не убежден в корректности типов, в Идрисе есть специальные доказательства и интерактивный режим их построения посредством тактик. Пишем так:
Знак вопроса перед равенством означает, что корректность типов будет доказана отдельной леммой. Запускаем REPL. Он такой код принимает, а по команде :m показывает, что есть одна незакрытая лемма:
Спрашиваем ее тип:
Тип ее говорит, что это функция, принимающая на вход аргументы исходной функции и ее результат в том виде, который смог вывести компилятор (Vect a (n + (S O)), где S O - это натуральная единица в унарной записи), а на выходе должна выдать значение того типа, что мы указали выходным типом исходной функции (Vect a (S n)). Перехожу в интерактивный режим доказательств: :p rotLeft_lemma_1, после чего как та мартышка с печатной машинкой пробую разные тактики (это был мой первый опыт, я тогда об этих тактиках почти ничего не знал). Вижу, что последовательность из двух тактик intros и trivial лемму закрывает:
Команда qed там выводит запись доказательства, которую можно скопировать в исходник. Копирую, компилирую, все проходит успешно, ура! Пробую запустить и вижу, что работает-то код неправильно: rotLeft [1,2,3] выдает [1,2,3] вместо ожидаемого [2,3,1]. Вот так, используя зависимые типы и теорем-прувинг, можно неожиданно заставить корректный код работать некорректно. А дело тут вот в чем. Команда intros последовательно вводит все входные параметры в качестве предпосылок доказательства, записывает их в графу "дано". А команда trivial ищет среди уже доказанного терм, подходящий по форме к доказываемой цели. Поскольку на выходе мы хотим получить значение такого же типа, какое было на входе, прувер просто взял входное значение. Получилось, что лемма эта забивает на вычисленный телом функции результат и возвращает вместо него аргумент функции. Типы сошлись, все чудесно, но результат неверный.
Если же подойти к делу более осмысленно, то можно тактиками подсказать компилятору, что следует попытаться произвести вычисления (развернуть функцию сложения), затем ввести нужные парамерты в графу "дано", а потом применить лемму из стандартной библиотеки о коммутативности сложения. Тогда цель и вычисленное функцией значение окажутся с одинаковыми типами, и можно тактикой trivial взять нужное, на этот раз то самое. Вот так выглядит рабочее доказательство:
C ним уже код работает правильно. Так-то.
rotLeft : Vect a (S n) -> Vect a (S n) rotLeft v = tail v ++ [head v]
А компилятор мне отвечает, что он хоть и умный, но ленивый, и ему не очевидно, что n+1 это n+1, ну в смысле что результат применения функции сложения натурального числа n с константой 1 это следующее за n число.
"Can't unify Vect a (m + n) with Vect a (S n)
Specifically: Can't unify plus m with S"
Для случаев, когда код правильный, но компилятор не убежден в корректности типов, в Идрисе есть специальные доказательства и интерактивный режим их построения посредством тактик. Пишем так:
rotLeft v ?= tail v ++ [head v]
Знак вопроса перед равенством означает, что корректность типов будет доказана отдельной леммой. Запускаем REPL. Он такой код принимает, а по команде :m показывает, что есть одна незакрытая лемма:
Global metavariables: [Main.rotLeft_lemma_1]
Спрашиваем ее тип:
> :t rotLeft_lemma_1 rotLeft_lemma_1 : (a : Type) -> (n : Nat) -> (Vect a (S n)) -> (Vect a (n + (S O))) -> Vect a (S n)
Тип ее говорит, что это функция, принимающая на вход аргументы исходной функции и ее результат в том виде, который смог вывести компилятор (Vect a (n + (S O)), где S O - это натуральная единица в унарной записи), а на выходе должна выдать значение того типа, что мы указали выходным типом исходной функции (Vect a (S n)). Перехожу в интерактивный режим доказательств: :p rotLeft_lemma_1, после чего как та мартышка с печатной машинкой пробую разные тактики (это был мой первый опыт, я тогда об этих тактиках почти ничего не знал). Вижу, что последовательность из двух тактик intros и trivial лемму закрывает:
> :p rotLeft_lemma_1
---------- Goal: ----------
{ hole 0 }:
(a : Type) ->
(n : Nat) ->
(Vect a (S n)) ->
(Vect a (n + (S O))) -> Vect a (S n)
-Main.rotLeft_lemma_1> intros
---------- Other goals: ----------
{ hole 3 }
{ hole 2 }
{ hole 1 }
{ hole 0 }
---------- Assumptions: ----------
a : Type
n : Nat
v : Vect a (S n)
value : Vect a (n + (S O))
---------- Goal: ----------
{ hole 4 }:
Vect a (S n)
-Main.rotLeft_lemma_1> trivial
rotLeft_lemma_1: No more goals.
-Main.rotLeft_lemma_1> qed
Main.rotLeft_lemma_1 = proof {
intros;
trivial;
}Команда qed там выводит запись доказательства, которую можно скопировать в исходник. Копирую, компилирую, все проходит успешно, ура! Пробую запустить и вижу, что работает-то код неправильно: rotLeft [1,2,3] выдает [1,2,3] вместо ожидаемого [2,3,1]. Вот так, используя зависимые типы и теорем-прувинг, можно неожиданно заставить корректный код работать некорректно. А дело тут вот в чем. Команда intros последовательно вводит все входные параметры в качестве предпосылок доказательства, записывает их в графу "дано". А команда trivial ищет среди уже доказанного терм, подходящий по форме к доказываемой цели. Поскольку на выходе мы хотим получить значение такого же типа, какое было на входе, прувер просто взял входное значение. Получилось, что лемма эта забивает на вычисленный телом функции результат и возвращает вместо него аргумент функции. Типы сошлись, все чудесно, но результат неверный.
Если же подойти к делу более осмысленно, то можно тактиками подсказать компилятору, что следует попытаться произвести вычисления (развернуть функцию сложения), затем ввести нужные парамерты в графу "дано", а потом применить лемму из стандартной библиотеки о коммутативности сложения. Тогда цель и вычисленное функцией значение окажутся с одинаковыми типами, и можно тактикой trivial взять нужное, на этот раз то самое. Вот так выглядит рабочее доказательство:
rotLeft_lemma_1 = proof {
compute; intro; intro; intro; intro;
rewrite plusCommutative n (S O); trivial;
}C ним уже код работает правильно. Так-то.
no subject
Date: 2013-02-11 09:04 am (UTC)> можно неожиданно заставить корректный код работать некорректно.
Ужасы какие
no subject
Date: 2013-02-11 09:51 am (UTC)no subject
Date: 2013-02-11 10:03 am (UTC)no subject
Date: 2015-10-15 10:37 pm (UTC)no subject
Date: 2013-02-11 11:03 am (UTC)no subject
Date: 2013-02-11 11:37 am (UTC)no subject
Date: 2013-02-11 12:31 pm (UTC)no subject
Date: 2013-02-11 12:52 pm (UTC)в рот мне ноги, computational term выписан ведь правильно, зачем тактики его меняют?
no subject
Date: 2013-02-11 01:46 pm (UTC)no subject
Date: 2013-02-11 04:51 pm (UTC)no subject
Date: 2013-02-11 05:19 pm (UTC)There is a weakness in provisional definitions at the minute, in that there is no guarantee that the value you find in the proof is at all related to the value it is supposed to have, other than in its type. I think if provisional definitions generated an equality constraint it would be better.
no subject
Date: 2013-02-11 05:22 pm (UTC)no subject
Date: 2013-02-11 06:47 pm (UTC)no subject
Date: 2013-02-12 03:29 am (UTC)no subject
Date: 2013-02-12 09:12 am (UTC)infixl 5 _∷ʳ_ _∷ʳ_ : ∀ {a n} {A : Set a} → Vec A n → A → Vec A (1 + n) [] ∷ʳ y = [ y ] (x ∷ xs) ∷ʳ y = x ∷ (xs ∷ʳ y)Ну и дальше всё выходит само собой, поскольку из определения плюса Агда "знает", что (1 + n) редуцируется к (suc n)rotLeft : ∀ {n} {A : Set} → Vec A (suc n) → Vec A (suc n) rotLeft vs = tail vs ∷ʳ head vsno subject
Date: 2013-02-12 10:08 am (UTC)Оператор + определен матчингом по первому аргументу, поэтому 1+n редуцируется элементарно, а вот n+1 уже фиг.
Но такое решение не указывает в типе все инварианты.
no subject
Date: 2013-02-12 10:10 am (UTC)no subject
Date: 2013-02-12 11:34 am (UTC)Или воспользоваться тем, что ℕ - коммутативное полукольцо, получив массу свойств на халяву (в том числе и нужную коммутативность).
После чего принудительно приводим типы с помощью subst
open import Relation.Binary.PropositionalEquality rotLeft : ∀ {n} {A : Set} → Vec A (suc n) → Vec A (suc n) rotLeft {n} {A} (v ∷ vs) = subst (Vec A) (sym (CS.+-comm 1 n)) (vs ++ v ∷ [])no subject
Date: 2013-02-12 11:57 am (UTC)no subject
Date: 2013-02-11 08:07 pm (UTC)А вообще после Coq, как только занялся Agda, понял что тактики (для меня) - это зло: вообще не понимаю, что происходит.
no subject
Date: 2013-02-12 03:26 am (UTC)