Hoy en día, los computadores son capaces de representar conceptos matemáticos en un lenguaje formal:
¿De qué se trata cuando formalizamos matemáticas?
Image credits: Assia Mahboubi.
Afirmación: Es posible construir gran parte de las matemáticas modernas basándose en la teoría intuicionista de tipos, que se debe a Per Martin-Löf (1972).
Los tres conceptos mayores son:
Π
-tipos).La implementación informática está basada en el Cálculo de Construcciones (Thierry Coquand, 1980s). Para ir más lejos, se pueden incorporar conceptos de Teoría Homotópica de Tipos.
Nat
o ℕ
y cuyos términos son 0, 1, 2, ...
. Como veremos, esto es un ejemplo de tipo inductivo.Si X
y Y
son tipos, podemos conformar el tipo X → Y
, cuyos términos son las funciones de X
a Y
, los cuales se definen así:
#check (ℕ → ℕ) -- ℕ → ℕ : Type
def f := fun (n : ℕ) => 2 * n
#check f -- f (n : ℕ) : ℕ
#check @f -- f : ℕ → ℕ
Además, algunos cálculos básicos pueden llevarse a cabo directamente en el compilador:
#reduce f 21 -- 42
La implementación de los enteros naturales como tipo inductivo utiliza los axiomas de Peano.
inductive Nat
| cero : Nat
| suc : Nat → Nat
Es decir que cero
es un término de tipo Nat
y que, para todo término n
de tipo Nat
, tenemos un término succ n
.
Esa declaración define funciones Nat.cero
y Nat.suc
, llamadas constructores.
#check Nat.cero -- Nat.zero : Nat
#check @Nat.suc -- Nat.suc : Nat → Nat
Si X
y Y
son tipos, podemos conformar un tipo X × Y
, cuyos términos se construyen a partir de un término x : X
y un término y : Y
.
inductive Prod (X Y : Type)
| intro (x : X) (y : Y)
En este caso, hay un solo constructor, Prod.intro
, el cual nos dice cómo introducir términos de tipo X × Y
.
#check Prod.intro
-- Prod.intro (x : X) (y : Y) : Prod X Y
Se puede también utilizar la notación (x, y)
para el término Prod.intro (x : X) (y : Y)
.
Ahora, para eliminar términos de un tipo inductivo como X × Y
, se utiliza el método de búsqueda de patrones (pattern matching).
def pr₁ : X × Y → X :=
fun (t : X × Y) => match t with
| Prod.intro x y => x
#check @pr₁ -- pr₁ : X × Y → X
Para un tipo inductivo con varios constructores, se hace pattern matching sobre cada constructor.
Volviendo a la analogía entre el
P
y Q
son proposiciones ya definidas. Entonces cómo definir la proposición P ∧ Q
?P
y Q
como tipos, entonces la proposición P ∧ Q
es simplemente el tipo producto de P
y Q
definido anteriormente!P ∧ Q
es, por definición, un par (p, q)
donde p
es una demostración de P
y q
es una demostración de Q
.P
y Q
son proposiciones ya definidas. Entonces qué significa decir que P
implica Q
?P
y Q
como tipos, entonces la proposición P → Q
es el tipo de funciones de P
a Q
y pensamos en tal función como una demostración de la proposición P → Q
.pr₁ : P × Q → P
que definimos anteriormente ahora se interpreta como una demostración de la proposición P ∧ Q → P
. En particular, tal implicación es una tautología.Dado tipos X
y Y
, podemos construir un tipo inductivo llamado suma (o coproducto) de X
y Y
.
inductive Sum (X Y : Type)
| from_left (x : X)
| from_right (y : Y)
#check @Sum.from_left -- @Sum.from_left : X → Sum X Y
#check @Sum.from_right -- @Sum.from_right : Y → Sum X Y
La interpretación en términos de proposiciones es la disyunción P ∨ Q
. Y la implicación P → P ∨ Q
es una tautología, porque tenemos la función Sum.from_left : P → P ∨ Q
.
P x
es una familia de proposiciones parametrizada por un término x: X
, qué significa decir que ∀ x, P x
? Por ejemplo, ∀ x : ℝ, x ^ 2 ≥ 0
.x
de tipo X
, el término P x
es una proposición, es decir un tipo que depende de x.x : X
a una demostración de P x
, es decir una función cuyo tipo de retorno depende del término de entrada.P x
como una familia de tipos.∀ x, P x
significa, por definición, construir una función dependientef : (x : X) → P x
.P
se ve como una funciónP : X → Type
, donde Type
es el universo de los tipos y (x : X) → P x
es el Π
-tipo (Pi-tipo) asociado a P
.Podemos asociarle a la familia P : X → Type
un tipo inductivo llamdo Σ
-tipo, cuyos términos son los pares (x, y)
donde x: X
y y : P x
.
inductive Σ (X : Type) (P : X → Type)
| intro (x : X) (y : P x)
Si P : X → Type
es constante, entonces volvemos a encontrar el producto X × Y
. Nótese que un término de tipo Σ X P
es de la forma Σ.intro x y
donde y : P x
. Tal término se interpreta como una demostración de la proposición ∃ x, P x
.
Consideremos el siguiente enunciado:
Teorema. Sean
n
ym
dos enteros naturales. Supongamos quen
es par o quem
es par. Entoncesn * m
es par.
Una formalización de esto podría parecerse a algo así:
∀ (n m : Nat), n.isEven ∨ m.isEven → (n * m).isEven
Es decir que tenemos que definir:
n.isEven
(parametrizada por n : Nat
).n.isEven ∨ m.isEven → (n * m).isEven
(parametrizada por (n m : Nat)
).Decir que n : Nat
es par significa ∃ k : Nat, n = 2 * k
. Esto se formaliza mediante:
k : Nat
.n = 2 * k
, es decir un término p
de tipo n = 2 * k
.La definición formal puede ser la siguiente
inductive Nat.isEven (n : Nat)
| intro (k : Nat) (p : n = 2 * k)
Es decir que podemos pensar en un término t : Nat.isEven n
como un par dependiente (k,p)
, donde p : n = 2 * k
.
n.isEven
Tenemos tipos bien definidos pero que no necesariamente tienen términos: un tipo puede tener habitantes o no.
#check isEven 2 -- Nat.isEven 2 : Type
#check isEven 3 -- Nat.isEven 3 : Type
Gracias a la manera como definimos el tipo isEven
, también tenemos acceso a la siguiente notación:
variable (m : Nat)
#check m.isEven -- Nat.isEven m : Type
#check (2 : Nat).isEven -- Nat.isEven 2 : Type
Podemos mostrar que el tipo isEven 2
tiene habitantes:
def two_is_even : Nat.isEven 2 := Nat.isEven.intro 1 rfl
Es decir que two_is_even
es el par dependiente (1, rfl)
donde rfl : 2 = 2 * 1
. Más precisamente, la prueba rfl
de que 2 = 2 * 1
es el término Eq.refl 2
, el cual dice que 2 ≡ 2 * 1
en el compilador (por definición de *
en Nat
).
De igual manera, los múltiplos de 2
son pares:
def mult_of_2_is_even (m : Nat) : (2 * m).isEven :=
Nat.isEven.intro m rfl
n.isEven → (n * m).isEven
La demostración matemática es la siguiente:
Si
n
es par, entonces existek
tal quen = 2 * k
, luego
n * m = (2 * k) * m = 2 * (k * m)
y eso demuestra quen * m
es par.
Y la prueba formal es la siguiente:
def Lema1 : (n m : Nat) → n.isEven → (n * m).isEven :=
fun n m (Nat.isEven.intro (k : Nat) (p : n = 2 * k)) =>
let q : (n * m) = 2 * (k * m) := -- definición local
Eq.trans (congrArg (fun x => x * m) p) (Nat.mul_assoc 2 k m)
Nat.isEven.intro (k * m) q
Es decir que una prueba formal es un programa.
m.isEven → (n * m).isEven
Queremos formalizar el siguiente argumento:
Si
m = 2 * k
, entoncesn * m = n * (2 * k) = (n * 2) * k = (2 * n) * k = 2 * (n * k)
.
Podemos incorporar el cálculo en el programa, justificando cada paso:
def Lema2 : (n m : Nat) → m.isEven → (n * m).isEven :=
fun n m (Nat.isEven.intro (k : Nat) (p : m = 2 * k)) =>
let q : n * m = 2 * (n * k) :=
calc
n * m = n * (2 * k) := congrArg (fun x => n * x) p
_ = (n * 2) * k := Eq.symm (Nat.mul_assoc n 2 k)
_ = (2 * n) * k := congrArg (fun x => x * k) (Eq.symm (Nat.mul_comm 2 n))
_ = 2 * (n * k) := Nat.mul_assoc 2 n k
Nat.isEven.intro (n * k) q
Ya con eso podemos demostrar que
n.isEven ∨ m.isEven → (n * m).isEven
La demostración se hace por pattern matching en los términos de tipo n.isEven ∨ m.isEven
y utiliza las funciones Lema1
y Lema2
.
def Teorema : (n m : Nat) → n.isEven ∨ m.isEven → (n * m).isEven :=
fun n m t => match t with
| ∨.from_left (t : n.isEven) => Lema1 n m t
| ∨.from_right (t : m.isEven) => Lema2 n m t
Demostración de nuestro teorema con el modo táctico de Lean:
import Mathlib.Tactic.Cases
import Mathlib.Tactic.Use
def Nat.isEven (n : Nat) : Prop := ∃ k : Nat, n = 2 * k
theorem Coloquio2024 : ∀ (n m : Nat),
n.isEven ∨ m.isEven → (n * m).isEven := by
intro n m t
cases' t with h1 h2
· cases' h1 with k1 p1
unfold Nat.isEven; use (k1 * m);
rw [p1, Nat.mul_assoc]
· cases' h2 with k2 p2
unfold Nat.isEven; use (n * k2)
rw [p2, ← Nat.mul_assoc, Nat.mul_comm n 2, Nat.mul_assoc]
(think of Leibniz, Peano, Russell and Whitehead, Bourbaki, ... ).
(be it a person or a machine: think of Pascal, Euler, ... ).
- The proof of the 4-color theorem shows that the theorem holds except perhaps in a finite number of cases. - Those $1834$ cases were then shown to be 4-colourable using various computer programs and algorithms, some of them written by J. Koch. Part of the proof was checked manually by Haken's daughter Dorothea Blohstein (born Haken).
- A recent example of the use of computers in mathematical research is provided by the [Liquid Tensor Experiment][LTE], which says that certain $Ext$ groups of the set of measures on a profinite set vanish. - This is part of D. Clausen and P. Scholze's approach to a general theory of analytic spaces, which is *currently being developed*.
--- ## Principio de inducción Al ser declarado el tipo `Nat` como un tipo inductivo, el compilador genera *automáticamente* un principio de inducción. Ahí vemos aparecer por primera vez el tipo `Prop`. ```haskell #check Nat.rec -- Nat.rec {P : ℕ → Prop} -- (cero : P 0) -- (suc : ∀ (n : ℕ), P n → P (n + 1)) : -- ∀ (t : ℕ), P t ``` In the code above, `Nat.rec` is seen as a function that, in the presence of a *predicate* `P : ℕ → Prop`, sends a proof of `P 0` and a proof of the fact that, for all `n : ℕ`, `P n` implies `P (n + 1)`, to a proof of `P t` for all `t : ℕ`.
En teoría homotópica de tipos, se ve a una familia `P : X → Type` como una fibración sobre `X` y a `f : (x : X) → P x` como una sección de esa fibración. Más precisamente, se contruye un tipo inductivo `Fib X P`, llamado `Σ`-tipo, cuyos términos son los pares `(x, y)` donde `x: X` y `y : P x`.
Our term `MyFirstProof` is recognised as a *dependent function*: it sends a natural number `n` to a proof of a statement *that depends on* `n`. ```haskell #check @MyFirstProof -- MyFirstProof : ∀ (n : ℕ), ∃ k, 4 * n = 2 * k #check MyFirstProof -- MyFirstProof (n : ℕ) : ∃ k, 4 * n = 2 * k ``` The term `MyFirstProof 2` is recognised as a proof of the proposition `∃ k, 4 * 2 = 2 * k` and we can use it as such. ```haskell #check MyFirstProof 2 -- MyFirstProof 2 : ∃ k, 4 * 2 = 2 * k def EightIsEven : ∃ m : ℕ, 8 = 2 * m := MyFirstProof 2 ```