Ответ 1
Ваша интуиция для bool_ind
находится на месте, но размышление о том, почему bool_ind
означает то, что вы сказали, может помочь прояснить два других. Мы знаем, что
bool_ind : forall P : bool -> Prop,
P true ->
P false ->
forall b : bool,
P b
Если мы читаем это как логическую формулу, мы получаем то же самое, что вы сделали:
- Для каждого предиката
P
наbool
eans,- Если
P true
выполняется, и - Если
P false
выполняется, то - Для каждого булева
b
-
P b
.
-
- Если
Но это не просто логическая формула, это тип. В частности, это (зависимый) тип функции. И как тип функции, он говорит (если вы позволите мне свободу изобретать имена для неназванных аргументов и результата):
- Учитывая значение
P : bool -> Prop
,- Значение
Pt : P true
, - Значение
Pf : P false
и - Значение
b : bool
,- Мы можем построить значение
Pb : P b
.
- Мы можем построить значение
- Значение
(Конечно, это каррическая функция, поэтому есть другие способы разбить тип на прозу, но это ясно для наших целей.)
Важнейшая вещь здесь, то, что делает Coq работой как теоретиком, будучи языком программирования (или наоборот), является Карри-Говардом: типы - это предложения, а значения - это доказательства этих предложений. Например, простой тип функции ->
соответствует импликации, а зависимый тип функции forall
соответствует универсальной квантификации. (Обозначение довольно наводящий:-)) Итак, в Coq, чтобы доказать, что φ → ψ, мы должны построить значение типа φ -> ψ
: функцию, которая принимает значение типа φ
(или, другими словами, a доказательство предложения φ) и использует его для построения значения типа ψ
(доказательство предложения ψ).
В Coq мы можем думать обо всех типах таким образом, живут ли эти типы в Set
, Type
или Prop
. (Итак, когда вы говорите: "Кажется, что P true (это Set for bool rec и Type for bool_rect) рассматривается как пропозициональное значение," вы правы!) Например, рассмотрим, как мы Внесите bool_ind
себя. Мы начнем с перечисления всех параметров функции вместе с возвращаемым типом:
Definition bool_ind' (P : bool -> Prop)
(Pt : P true)
(Pf : P false)
(b : bool)
: P b :=
До сих пор так хорошо. На этом этапе мы хотели бы вернуть что-то типа P b
, но мы не знаем, что такое b
. Итак, как всегда в этих ситуациях, мы сопоставляем шаблон:
match b with
В настоящее время существует два случая. Во-первых, b
может быть true
. В этом случае нам нужно вернуть что-то типа P true
, и, к счастью, мы имеем такое значение: Pt
.
| true => Pt
Случай false
аналогичен:
| false => Pf
end.
Обратите внимание, что когда мы реализуем bool_ind'
, это выглядит не очень "доказательством", а скорее "программированием". Конечно, благодаря переписке Карри-Говарда, это одно и то же. Но обратите внимание, что для двух других функций достаточно одной и той же реализации:
Definition bool_rec' (P : bool -> Set)
(Pt : P true)
(Pf : P false)
(b : bool)
: P b :=
match b with
| true => Pt
| false => Pf
end.
Definition bool_rect' (P : bool -> Type)
(Pt : P true)
(Pf : P false)
(b : bool)
: P b :=
match b with
| true => Pt
| false => Pf
end.
Глядя на это вычислительное определение, вы обнаруживаете другой способ вещи о bool_ind
, bool_rec
и bool_rect
: они инкапсулируют то, что вам нужно знать, чтобы говорить о каждом значении bool
. Но в любом случае мы собираем эту информацию: если я что-то знаю для true
и что-то для false
, то я знаю ее для всех bool
s.
Определение функций bool_{ind,rec,rect}
абстрагируется обычным образом, мы пишем функции по булеву: там один аргумент, соответствующий истинной ветки, и один - в ложную ветвь. Или, другими словами: эти функции являются просто операторами if
. На языке, не зависящем от языка, они могут иметь более простой тип forall S : Set, S -> S -> bool -> S
:
Definition bool_simple_rec (S : Set) (St : P) (Sf : P) (b : bool) : S :=
match b with
| true => St
| false => Sf
end.
Однако, поскольку типы могут зависеть от значений, мы должны прокручивать b
через типы везде. Если окажется, что мы этого не хотим, мы можем использовать нашу более общую функцию и сказать:
Definition bool_simple_rec' (S : Set) : S -> S -> bool -> S :=
bool_rec (fun _ => S).
Никто никогда не говорил, что наш P : bool -> Set
должен был использовать bool
!
Все эти функции намного интереснее для рекурсивных типов. Например, Coq имеет следующий тип натуральных чисел:
Inductive nat : Set := O : nat | S : nat -> nat.
И мы имеем
nat_ind : forall P : nat -> Prop,
P O ->
(forall n' : nat, P n' -> P (S n')) ->
forall n : nat,
P n
Наряду с соответствующими nat_rec
и nat_rect
. (Упражнение для читателя: непосредственно реализуйте эти функции.)
На первый взгляд, это всего лишь принцип математической индукции. Однако также, как мы пишем рекурсивные функции на nat
s; они то же самое. В общем случае рекурсивные функции над nat
выглядят следующим образом:
fix f n => match n with
| O => ...
| S n' => ... f n' ...
end
Плечо совпадения после O
(базовый регистр) - это просто значение типа P O
. Рукой соответствия после S n'
(рекурсивный случай) является то, что передается в функцию типа forall n' : nat, P n' -> P (S n')
: n'
совпадают, а значение P n'
является результатом рекурсивного вызова f n'
.
Другой способ думать об эквивалентности между функциями _rec
и _ind
, а затем - и тот, который, я думаю, более ясен на бесконечных типах, чем на bool
, - это то же, что эквивалентность между математическим < (что происходит в Prop
) и (структурном) rec
ursion (что происходит в Set
и Type
).
Пусть получаются праксические и используют эти функции. Мы определим простую функцию, которая преобразует булевы в натуральные числа, и мы сделаем это как напрямую, так и с помощью bool_rec
. Самый простой способ записи этой функции - совпадение с шаблоном:
Definition bool_to_nat_match (b : bool) : nat :=
match b with
| true => 1
| false => 0
end.
Альтернативное определение
Definition bool_to_nat_rec : bool -> nat :=
bool_rec (fun _ => nat) 1 0.
И эти две функции одинаковы:
Goal bool_to_nat_match = bool_to_nat_rec.
Proof. reflexivity. Qed.
(Примечание: эти функции синтаксически равны. Это более сильное условие, чем просто выполнение одной и той же вещи.)
Здесь P : bool -> Set
- fun _ => nat
; он дает нам тип возврата, который не зависит от аргумента. Наша Pt : P true
- это 1
, вещь, которую нужно вычислить, когда нам дано true
; аналогично, наш Pf : P false
равен 0
.
Если мы хотим использовать зависимость, мы должны подготовить полезный тип данных. Как насчет
Inductive has_if (A : Type) : bool -> Type :=
| has : A -> has_if A true
| lacks : has_if A false.
С этим определением has_if A true
изоморфно A
, а has_if A false
изоморфно unit
. Тогда мы могли бы иметь функцию, которая сохраняет свой первый аргумент тогда и только тогда, когда она прошла true
.
Definition keep_if_match' (A : Type) (a : A) (b : bool) : has_if A b :=
match b with
| true => has A a
| false => lacks A
end.
Альтернативное определение
Definition keep_if_rect (A : Type) (a : A) : forall b : bool, has_if A b :=
bool_rect (has_if A) (has A a) (lacks A).
И они снова совпадают:
Goal keep_if_match = keep_if_rect.
Proof. reflexivity. Qed.
Здесь возвращаемый тип функции зависит от аргумента b
, поэтому наш P : bool -> Type
действительно что-то делает.
Вот более интересный пример, в котором используются натуральные числа и индексированные по длине списки. Если вы не видели индексированные по длине списки, также называемые векторами, они точно такие, что они говорят на олове; vec A n
- это список n
A
s.
Inductive vec (A : Type) : nat -> Type :=
| vnil : vec A O
| vcons : forall n, A -> vec A n -> vec A (S n).
Arguments vnil {A}.
Arguments vcons {A n} _ _.
(Механизм Arguments
обрабатывает неявные аргументы.) Теперь мы хотим создать список n
копий некоторого определенного элемента, поэтому мы можем написать это с помощью fixpoint:
Fixpoint vreplicate_fix {A : Type} (n : nat) (a : A) : vec A n :=
match n with
| O => vnil
| S n' => vcons a (vreplicate_fix n' a)
end.
В качестве альтернативы мы можем использовать nat_rect
:
Definition vreplicate_rect {A : Type} (n : nat) (a : A) : vec A n :=
nat_rect (vec A) vnil (fun n' v => vcons a v) n.
Обратите внимание, что поскольку nat_rect
фиксирует шаблон рекурсии, vreplicate_rect
не является самой фиксированной точкой. Следует отметить третий аргумент nat_rect
:
fun n' v => vcons a v
v
концептуально является результатом рекурсивного вызова vreplicate_rect n' a
; nat_rect
абстрагируется от рекурсивного шаблона, поэтому нам не нужно называть его напрямую. n'
действительно тот же n'
, что и в vreplicate_fix
, но теперь кажется, что нам не нужно упоминать его явно. Почему он прошел? Это становится ясным, если мы выписываем наши типы:
fun (n' : nat) (v : vec A n') => vcons a v : vec A (S n')
Нам нужно n'
, чтобы мы знали, какой тип v
имеет, и, следовательно, какой тип имеет результат.
Посмотрите эти функции в действии:
Eval simpl in vreplicate_fix 0 tt.
Eval simpl in vreplicate_rect 0 tt.
(* both => = vnil : vec unit 0 *)
Eval simpl in vreplicate_fix 3 true.
Eval simpl in vreplicate_rect 3 true.
(* both => = vcons true (vcons true (vcons true vnil)) : vec bool 3 *)
И действительно, они одинаковы:
(* Note: these two functions do the same thing, but are not syntactically
equal; the former is a fixpoint, the latter is a function which returns a
fixpoint. This sort of equality is all you generally need in practice. *)
Goal forall (A : Type) (a : A) (n : nat),
vreplicate_fix n a = vreplicate_rect n a.
Proof. induction n; [|simpl; rewrite IHn]; reflexivity. Qed.
Выше, я поставил задачу переопределения nat_rect
и друзей. Вот ответ:
Fixpoint nat_rect' (P : nat -> Type)
(base_case : P 0)
(recurse : forall n', P n' -> P (S n'))
(n : nat)
: P n :=
match n with
| O => base_case
| S n' => recurse n' (nat_rect' P base_case recurse n')
end.
Это, надеюсь, дает понять, как nat_rect
абстрагирует рекурсионный шаблон и почему он достаточно общий.