mirror of
https://github.com/saymrwulf/verifying-crypto-with-lean.git
synced 2026-09-03 19:53:45 +00:00
209 lines
8.8 KiB
TeX
209 lines
8.8 KiB
TeX
|
|
\chapter{Propositions as Types: The Idea That Makes It All Work}
|
||
|
|
\label{ch:pat}
|
||
|
|
|
||
|
|
\section{A suspicious similarity}
|
||
|
|
|
||
|
|
Here are two things that look unrelated. First, a function that converts a
|
||
|
|
pair into something else:
|
||
|
|
|
||
|
|
\begin{lstlisting}[language=Lean]
|
||
|
|
def swap (pair : A x B) : B x A := (pair.2, pair.1)
|
||
|
|
\end{lstlisting}
|
||
|
|
|
||
|
|
Second, a fact of logic: \emph{if $A$ and $B$ both hold, then $B$ and $A$ both
|
||
|
|
hold.} To prove it, you would say: ``suppose I have evidence for $A$ and
|
||
|
|
evidence for $B$; then I can produce evidence for $B$ and evidence for $A$ ---
|
||
|
|
just present the same two pieces in the other order.''
|
||
|
|
|
||
|
|
That prose proof and that program are the \emph{same object}. The function
|
||
|
|
takes a pair of values and reorders it; the proof takes a pair of pieces of
|
||
|
|
evidence and reorders it. This is not an analogy or a teaching trick. It is a
|
||
|
|
theorem about logic and computation, discovered independently by logicians
|
||
|
|
(Curry, Howard) and now the load-bearing wall of Lean:
|
||
|
|
|
||
|
|
\begin{bigidea}
|
||
|
|
\textbf{The Curry--Howard correspondence.} A proposition can be read as a
|
||
|
|
type --- the type of its proofs. A proof is then simply a \emph{program} of
|
||
|
|
that type. Checking a proof is type-checking a program. There is no separate
|
||
|
|
``proof checker'' bolted onto Lean: the type checker you met in
|
||
|
|
Chapter~\ref{ch:lean} \emph{is} the proof checker.
|
||
|
|
\begin{center}
|
||
|
|
\begin{tabular}{@{}lll@{}}
|
||
|
|
\toprule
|
||
|
|
\textbf{Logic} & \textbf{Programming} & \textbf{In Lean} \\
|
||
|
|
\midrule
|
||
|
|
proposition $P$ & type & \lean{P : Prop} \\
|
||
|
|
proof of $P$ & value/program of that type & \lean{h : P} \\
|
||
|
|
$P \to Q$ (implication)& function type & \lean{P -> Q} \\
|
||
|
|
$P \land Q$ (and) & pair type & \lean{P /\ Q} \\
|
||
|
|
$P \lor Q$ (or) & tagged union & \lean{P \/ Q} \\
|
||
|
|
$\lnot P$ (not) & \lean{P -> False} & \lean{Not P} \\
|
||
|
|
``true'' & type with one trivial value & \lean{True} \\
|
||
|
|
``false'' & \emph{empty} type & \lean{False} \\
|
||
|
|
\bottomrule
|
||
|
|
\end{tabular}
|
||
|
|
\end{center}
|
||
|
|
\end{bigidea}
|
||
|
|
|
||
|
|
Take a minute with the last row: \lean{False} is a type with \emph{no
|
||
|
|
constructors} --- no way to build a value. To prove a false statement you
|
||
|
|
would have to produce an inhabitant of an empty type. That is why the system
|
||
|
|
is sound: lies have no evidence, so lies do not type-check.
|
||
|
|
|
||
|
|
\section{Proofs are programs: first proofs}
|
||
|
|
|
||
|
|
Let us write actual proofs as actual programs. Implication is a function
|
||
|
|
type, so proving ``$P$ implies $P$'' means writing the identity function:
|
||
|
|
|
||
|
|
\begin{lstlisting}[language=Lean]
|
||
|
|
theorem p_implies_p (P : Prop) : P -> P :=
|
||
|
|
fun h => h
|
||
|
|
\end{lstlisting}
|
||
|
|
|
||
|
|
Read \lean{fun h => h} aloud as a proof: ``assume $P$ holds --- call the
|
||
|
|
evidence $h$; then $P$ holds, by $h$.'' Every classical proof-writing phrase
|
||
|
|
has a program shape:
|
||
|
|
|
||
|
|
\begin{center}
|
||
|
|
\begin{tabular}{@{}ll@{}}
|
||
|
|
\toprule
|
||
|
|
\textbf{You say in prose} & \textbf{You write in Lean} \\
|
||
|
|
\midrule
|
||
|
|
``Assume $P$; call it $h$'' & \lean{fun h => ...} \\
|
||
|
|
``By hypothesis $h$'' & \lean{h} \\
|
||
|
|
``Apply lemma $f$ to fact $h$'' & \lean{f h} \\
|
||
|
|
``Both parts hold: ... and ...'' & \lean{And.intro pf1 pf2} \\
|
||
|
|
``From $h : P \land Q$, the first part'' & \lean{h.1} \\
|
||
|
|
\bottomrule
|
||
|
|
\end{tabular}
|
||
|
|
\end{center}
|
||
|
|
|
||
|
|
The pair-swapping example, now as an official theorem:
|
||
|
|
|
||
|
|
\begin{lstlisting}[language=Lean]
|
||
|
|
theorem and_swap (P Q : Prop) : P /\ Q -> Q /\ P :=
|
||
|
|
fun h => And.intro h.2 h.1
|
||
|
|
\end{lstlisting}
|
||
|
|
|
||
|
|
And transitivity of implication is exactly function composition:
|
||
|
|
|
||
|
|
\begin{lstlisting}[language=Lean]
|
||
|
|
theorem imp_trans (P Q R : Prop) : (P -> Q) -> (Q -> R) -> (P -> R) :=
|
||
|
|
fun pq qr => fun p => qr (pq p)
|
||
|
|
\end{lstlisting}
|
||
|
|
|
||
|
|
\begin{aha}
|
||
|
|
If you have ever composed two functions, you have already done everything
|
||
|
|
this proof does. The intimidating part of formal logic --- ``natural
|
||
|
|
deduction,'' ``inference rules'' --- turns out to be the part you knew from
|
||
|
|
programming all along. What logicians call \emph{modus ponens}, you call
|
||
|
|
\emph{calling a function}.
|
||
|
|
\end{aha}
|
||
|
|
|
||
|
|
\section{Equality and the proof that \texorpdfstring{$1+1=2$}{1+1=2}}
|
||
|
|
|
||
|
|
The proposition $a = b$ is also a type. Its only constructor is reflexivity
|
||
|
|
--- \lean{rfl} --- which proves \lean{a = a}. How can that ever prove
|
||
|
|
anything interesting? Because Lean \emph{computes} before comparing:
|
||
|
|
|
||
|
|
\begin{lstlisting}[language=Lean]
|
||
|
|
theorem one_plus_one : 1 + 1 = 2 := rfl
|
||
|
|
\end{lstlisting}
|
||
|
|
|
||
|
|
Lean unfolds \lean{1 + 1} using the two-line definition of addition from
|
||
|
|
Chapter~\ref{ch:lean}, arrives at \lean{2}, and sees that both sides are
|
||
|
|
\emph{literally the same value}. The equation holds by computation. This
|
||
|
|
mechanism --- \emph{definitional equality} --- is the engine that lets proofs
|
||
|
|
lean on programs, and later lets us prove facts about extracted Rust code by,
|
||
|
|
in part, just running it symbolically.
|
||
|
|
|
||
|
|
\begin{pitfall}
|
||
|
|
\lean{rfl} proves $2^{255} - 19$-sized computations happily, but it can only
|
||
|
|
prove what computation alone can see. \lean{n + 0 = n} is \lean{rfl} (the
|
||
|
|
definition's first equation matches), yet \lean{0 + n = n} is \emph{not} ---
|
||
|
|
recursion is on the \emph{second} argument, and \lean{n} is an opaque
|
||
|
|
variable, so nothing unfolds. The statement is still true; it just needs a
|
||
|
|
real proof (induction --- next chapter). The asymmetry feels unfair for about
|
||
|
|
a day. Then it becomes your sharpest mental model of what a computer can and
|
||
|
|
cannot know for free.
|
||
|
|
\end{pitfall}
|
||
|
|
|
||
|
|
\section{Universals, existentials, and dependent types}
|
||
|
|
|
||
|
|
Cryptographic specifications are universal statements: ``\emph{for all}
|
||
|
|
inputs, the output is correct.'' In Lean, $\forall$ is a function type whose
|
||
|
|
\emph{result type mentions the argument}:
|
||
|
|
|
||
|
|
\begin{lstlisting}[language=Lean]
|
||
|
|
theorem add_self_even : forall n : Nat, isEven (n + n) = true := ...
|
||
|
|
\end{lstlisting}
|
||
|
|
|
||
|
|
A proof of \lean{forall n, P n} is a function that eats any \lean{n} and
|
||
|
|
returns a proof of \lean{P n} --- one uniform recipe covering all the
|
||
|
|
infinitely many cases at once. This is precisely the thing testing could not
|
||
|
|
give us in Chapter~\ref{ch:why}: testing produces finitely many
|
||
|
|
\lean{P 3, P 17, P 42}; a proof produces the function.
|
||
|
|
|
||
|
|
Dually, \lean{exists n, P n} is proved by handing over a concrete witness
|
||
|
|
together with evidence: \lean{Exists.intro 4 pf}. And remember the
|
||
|
|
\lean{Rational} exercise from last chapter --- the denominator you could not
|
||
|
|
keep nonzero? Dependent types fix it by letting data carry proofs:
|
||
|
|
|
||
|
|
\begin{lstlisting}[language=Lean]
|
||
|
|
structure Rational where
|
||
|
|
num : Int
|
||
|
|
den : Nat
|
||
|
|
den_ne_zero : den ≠ 0 -- a PROOF, stored inside the value
|
||
|
|
\end{lstlisting}
|
||
|
|
|
||
|
|
No value of this type with a zero denominator can ever be constructed,
|
||
|
|
anywhere, by anyone. In the real Ed25519 development this exact pattern
|
||
|
|
appears as a \emph{bounds invariant}: a field element travels together with
|
||
|
|
the proof that its five limbs are small enough not to overflow the next
|
||
|
|
multiplication. The data structure makes the unsafe states unrepresentable.
|
||
|
|
|
||
|
|
\section{What about proof by contradiction?}
|
||
|
|
|
||
|
|
One more resident of the logical zoo. Lean's core logic is
|
||
|
|
\emph{constructive}: a proof of existence builds a witness. Classical
|
||
|
|
reasoning --- ``it's either true or false, and not false, hence true'' --- is
|
||
|
|
available the moment you want it (Mathlib imports it as \lean{Classical.choice},
|
||
|
|
and we will meet it again on the trust ledger in Chapter~\ref{ch:honesty}),
|
||
|
|
but it is an \emph{ingredient you can see}, not smuggled seasoning. When a
|
||
|
|
verification result says ``this proof uses only \lean{propext},
|
||
|
|
\lean{Classical.choice}, \lean{Quot.sound},'' that is a complete list of the
|
||
|
|
logical beliefs you are being asked to hold. Three. You can audit them over
|
||
|
|
coffee.
|
||
|
|
|
||
|
|
\begin{tryit}
|
||
|
|
Open \code{exercises/Ch03.lean} and prove, as programs (no tactics yet!):
|
||
|
|
\lean{P -> Q -> P}; \ \lean{(P /\ Q) -> (P \/ Q)}; \ and modus ponens
|
||
|
|
\lean{P -> (P -> Q) -> Q}. Each is a one-liner. Feel free to be delighted
|
||
|
|
when the pieces click together like typed Lego.
|
||
|
|
\end{tryit}
|
||
|
|
|
||
|
|
\section*{Exercises}
|
||
|
|
|
||
|
|
\exercise{Prove \lean{and_assoc : (P /\ Q) /\ R -> P /\ (Q /\ R)} as a
|
||
|
|
term-mode program using \lean{h.1}, \lean{h.2}, and \lean{And.intro}.}
|
||
|
|
|
||
|
|
\exercise{Prove \lean{or_swap : P \/ Q -> Q \/ P}. You will need case
|
||
|
|
analysis on which side holds: \lean{match h with | Or.inl p => ... | Or.inr q => ...}}
|
||
|
|
|
||
|
|
\exercise{\lean{Not P} is \emph{defined} as \lean{P -> False}. Using only
|
||
|
|
that, prove \lean{P -> Not (Not P)}. Write down in one sentence what program
|
||
|
|
you just wrote.}
|
||
|
|
|
||
|
|
\exercise{(Thought) Explain to a skeptical friend why a type with no
|
||
|
|
constructors is the right representation of falsehood --- and what would go
|
||
|
|
wrong with the whole edifice if someone added a constructor to it.}
|
||
|
|
|
||
|
|
\begin{checkpoint}
|
||
|
|
You should now be able to: translate each logical connective into its type
|
||
|
|
($\to$, $\land$, $\lor$, $\lnot$, $\forall$, $\exists$); write small proofs
|
||
|
|
as terms; explain why \lean{rfl} proves \lean{1 + 1 = 2} but not
|
||
|
|
\lean{0 + n = n}; and articulate the Curry--Howard slogan --- \emph{proofs
|
||
|
|
are programs, propositions are types, checking is type-checking} --- with a
|
||
|
|
straight face and genuine conviction.
|
||
|
|
\end{checkpoint}
|