Как может coq Set или Type быть предложением

Я читаю учебник по Coq. Он создает тип bool следующим образом:

Coq < Inductive bool :  Set := true | false.
bool is defined
bool_rect is defined
bool_ind is defined
bool_rec is defined

Затем он показывает, что каждая из этих вещей использует "Проверить".

Coq < Check bool_ind.
bool_ind
     : forall P : bool -> Prop, P true -> P false -> forall b : bool, P b

Coq < Check bool_rec.
bool_rec
     : forall P : bool -> Set, P true -> P false -> forall b : bool, P b

Coq < Check bool_rect.
bool_rect
     : forall P : bool -> Type, P true -> P false -> forall b : bool, P b

Я понимаю bool_ind. В нем говорится, что если что-то выполняется для true и оно выполняется для false, то оно выполняется для всех b в bool (потому что это единственные два).

Но я не понимаю, что означают выражения для bool_rec или bool_rect. Кажется, что P true (который является Set для bool_rec и a Type для bool_rect) рассматривается как пропозициональное значение. Что мне здесь не хватает?

Ответ 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 абстрагирует рекурсионный шаблон и почему он достаточно общий.