In my opinion it's a tragedy there are so few resources in using "Propositions as Types"/"Curry–Howard correspondence"[0] in didactics in tandem with teaching functional programming to teach structured proof-writing.
Many students do not feel comfortable with proof-writing and cannot dispatch/discharge implications or quantifications correctly when writing proofs and I believe that a structured approach using the Curry-Howard correspondence could help.
While I am first in line to say that programming is math whether you like it or not, it is certainly the case that few programmers are using that sort of math in their heads when they are programming. I suspect that if, oh, about 60-70 years of trying to change that has not had any headway that there is little reason to suspect that's going to change in the next 30.
But you can sort of backdoor the idea with something like this: https://jerf.org/iri/post/2025/fp_lessons_types_as_assertion... I won't claim it is exactly the same, but it is certainly similar. Tell a programmer, "Hey, you can build a type that carries certain guarantees with it, so that the rest of the program doesn't need to be constantly validating it", a problem any 5+ year dev ought to be very familiar with, and I think you make more progress in the direction of this notion than a paper full of logic diagrams in a notation even most computer scientist undergrads will never encounter.
(I'm not even certain if I directly encountered that in my Master's degree. It's been a while. I think I did, but I think it was only one class, and not a huge part of it. I'm not sure because I know I did more with it on my own, so I can't remember if I was formally taught it or if I picked it up entirely myself.)
While I generally agree with defining new types to assert that validation has been done, I think your blog post could have explained more about what kinds of validation are practical to do. For example:
> Address that represents a “street address” that has been validated by your street address to exist
What does it even mean to verify that a street address exists? Verifying real-world relationships is complex and error-prone. I’m reminded of the genre of articles starting with “Falsehoods programmers believe about names” [1].
In practice, the rules that can be enforced are often imposed by the system itself. It simply doesn’t accept data that isn’t in correct format or isn’t consistent with other data it already has. And we need to be cautious about what harm might be caused by these rejections.
Having the validation logic in one place will certainly help when fixing mistakes, but then what do you do about data that’s already been accepted, but is no longer “valid?” This sort of thing makes long-running systems like databases hard to maintain.
Note that article does explicitly have the sentence "Let’s forget the Umptydozen Falsehoods Programmers Believe About Addresses for the sake of argument and stipulate a perfect such function." These are examples. Trying to write "here's how to validate everything that could ever happen and all also here's a breakdown of all the falsehoods and also here's how it interacts with all your other logic" is not exactly a blog post so much as a book series. It turns out that even if you completely ignore the Umptydozen falsehoods of all the various kinds, you still have plenty of validation problems to talk about!
However, the in-a-nutshell answer to "how do you handle time invalidating things" is that you treat your database as an untrusted store and validate things as needed. I'm actually an 80/20 guy on using databases to maintain integrity for much this reason; I love me some foreign keys and I use them extensively but the truth is that that is only a partial solution to the data validity problem no matter how clever you get, and temporal inconsistency is a big one. Once you have any source of inconsistencies or errors in your DB, a whole whackload of need for validation and care basically comes dropping in all at once, or, to put it another way, if you're not 100.00000% successful at maintaining data integrity, the next practical option is 95%. There is no practical in-between, because even that .001% will end up driving you every bit as crazy as 5% being wrong in most ways.
But that's also out-of-scope for blog posts targeted at people who are only doing ad-hoc validation whenever they are forced to. Learn how to validate properly at all, then get better when you have a time-based problem.
Good article. Yeah, I wouldn’t expect a full explanation, just some kind of “here be dragons” caveat. Perhaps a hyperlink alone is a bit too subtle since readers aren’t always going to dereference it. (And there’s some irony there, given the subject of the linked article.)
The types in Go’s template/html package are a pretty interesting example of using types tactically to indicate validity. The HTML type is used to turn off HTML escaping when it’s already been done. It’s using a type as a loophole. It’s still very useful to have a type like that when reviewing code for security bugs, because you know where to look for problems. Unsafe sections in Rust serve a similar purpose.
Types are about creating trust, and this trust is often short-lived. When data crosses a security boundary, the validation has to done again.
Yeah, the issue is that proofs are harder than people think, even for trivial things (try a few easy leetcode problems in Lean with a proof of correctness), and less useful than people think, especially when you get past low level algorithms and into domain logic (your point exactly). They also don't serialize well, so a database or API call with a "proof" field would be susceptible to fudging, or the definition could change between deployments. They're also easy to make incompatible: one library requires bounds proofs for signed ints, but your app uses unsigned ints, so you have to either rewrite your app or rewrite the library, or cast, in which your type system has to handle like a "checked exception" and propagate that possibility throughout your whole type domain and app logic.
I'm pretty convinced there's a good reason that while "propositions as types" is cool in theory, it's unlikely we'll ever see it catch on in practice.
> try a few easy leetcode problems in Lean with a proof of correctness
This might actually be harder than, say, proving some undergraduate math theorems because reasoning about mutable state (especially when you want it to be performant) is tricky. I might guess that it could be easier to use a model checker such as TLA+ for that (although that can only verify algorithm descriptions and not implementations) because they seem to be more built with these things in my mind, but I lack enough experience with it to be certain.
> …especially when you get past low level algorithms and into domain logic (your point exactly). They also don't serialize well, so a database or API call with a "proof" field would be susceptible to fudging…
Nonsense.
Proving things about low-level algorithms that are full of complicated behaviors involving shared mutable state is often more difficult than proving things about high-level domain logic; regardless, people still do it, and if you use any modern CPU, especially from the ARM family, you benefit from their work.
A proof serializes just as well as any other kind of program — after all, the first step in checking a proof involves deserializing the proof, or in other words, reading and parsing its source code.
As for “fudging”; proofs are guaranteed to be correct up to the correctness of the proof checker, which can be used to recheck the proof at any time.
For more information, see Geuvers (2009) “Proof assistants: History, ideas and future”.
> Proving things about low-level algorithms [...] more difficult than proving things about high-level domain logic
I'm not sure I buy that. Like, prove that you never double-charge a sale, or a deactivated user can never make a purchase. Even if all that logic is within one service, it could be separated by multiple layers, callback functions and redirection, parallel tasks, database calls scattered between, etc. And even once you do have a proof, a single line code change anywhere between here and there could easily force you to redo the whole thing. And that doesn't even get into the more common case of distributed services.
> A proof serializes
We're probably talking about different things. I'm imagining something like a proof of the optimal traveling salesman between a bunch of cities. (Note, optimal here; verifying that a solution is below a specified limit can be done in polynomial time, but verifying optimality cannot). Say you want to store the answer in a database for later lookup. But it'd be possible that the DB could get fudged, and so "proof"-wise couldn't be trusted. Thus anything requiring a type-level proof would have to solve it from scratch every time. That's what I mean by "they don't serialize well" (though some proofs, like "N is not prime" can be serialized by storing the factors). Of course you could work around it and add "assuming our database has not been fudged" to the axioms, but the second you manually update one record in your database, the house of cards comes tumbling down and you can no longer trust that any of the proofs that you've worked so hard to build still apply in your system.
It isn't exactly the same. I believe I said that up front.
But it provides a bridge. Both things have "You can assert Property X about all things of type Y" built into them. Trying to jump people straight to C-H without giving any hint whatsoever of why this may be useful in any real code... well, that's a non-trivial percentage of why this approach has completely failed to capture any significant mindspace, though I leave it as an exercise to the reader to determine the exact percentage in their minds.
C-H has nothing to do with "property X is true about all things of type Y". It says "if you can find an object of type Y, you can prove global property Z."
For example, the property corresponding to the type "(Either a a) -> a" is "(A or A) implies A" which doesn't tell you any properties of actual objects of the type "(Either a a) -> a"
The property corresponding to the type "(Either a b) -> a" is "(A or B) implies A" which is not provable, so you can't find any object of this type.
One of the best modern resources is "Programming language foundations in Agda", co-authored by Wen Kokke and the same Philip Wadler, and used for teaching at multiple universities.
I've been thinking the same thing and mentioned it the other day[0]. When I was learning proofs, I saw people struggle with the idea of needing to choose a "generic" element to prove a forall statement, for example. I suspect it might make more sense if you taught things in terms of needing to write a function x=>P(x). I think in some cases, thinking in terms of programming might also change how we think about structuring things. e.g. define a data structure for a "point with error tolerance" (x, epsilon), then continuity says given a toleranced-point y* at f(x), I can find a toleranced-point x* at x such that f(x*) is "compatible" (equal at the base point and within tolerance) with y*. This factoring lets you avoid shocking new students with quantifier soup. Likewise the chain rule is just straightforward composition when you define a "derivative at a point" data structure Df((x,m))=(f(x), m*f'(x)).
This is not at all new, but I suppose it's currently a lot to ask students to learn proof mechanics while also unwinding multiple layers of definitions for abstractions. Computers can help do the unwinding (or lifting) automatically to make it easier to make small "quality of life" definitions that otherwise wouldn't be hugely useful when pen-and-paper-proofs would always be unwinding them anyway.
Basically, math education could look a lot like software engineering education. The concerns and solution patterns are basically the same. e.g. typeclasses are pretty much how mathematics does polymorphism, and are probably usually the right way to do it in programming too.
The connection to intuitionism (see https://en.wikipedia.org/wiki/Intuitionism) gives this kind of thinking much broader appeal and application. It seems to me we live in a time so dominated by analytical thinking that we completely ignore other useful and effective modes.
As others have explained, there is indeed nothing wrong with "proof by contradiction" of a negative statement. Intuitionistic logic does not view statements phrased in the "positive" and in the "negative" as fully equivalent, as classical logic does. (There are ways of formalizing this point of view quite rigorously, such as in so-called "ecumenical logics" where classical and constructive/intuitionistic reasoning can in fact coexist and interoperate, but statements derived from classical reasoning can only be translated in the negative as seen within constructive reasoning.)
So I asked specifically for you to assume that I don't already agree with intuitionist logic. Now, assuming that, what is wrong with proof by contradiction of a positive statement?
Both you and johnnyjeans gave me answers that already assumed intuitionism. I don't assume that. Can you give me any reasons that start from where I am and show my why I should adopt intuitionism?
I fully agree. Platonism isn't a hack, it is the only philosophy of math that makes sense to me. Something is either true, or it isn't. There is no third case. This is because mathematical objects are real, not just something our minds make up. To quote Arnold: Mathematics is the part of physics where experiments are cheap.
That said, I don't have a problem with intuitionistic logic, but I think about it as a platonist, for example via Kripke models. I also don't have a problem thinking about it in terms of a certain restricted class of proofs, that people call constructive proofs.
Because they explain nothing and aren't useful for building new insights. It's not a direct verification of the existence or lack-of-existence of a thing with given properties. It relies on rules lawyering in a very specific logic to say something with basically no substance. Allowing it causes more problems for automated proofs than they solve, so long as your domain only deals with finites (which all real-world domains do exclusively.)
They're also generally much easier to create than constructive proofs, which makes them a convenient go-to for lazy academics.
Proof by contradiction is fine for lack-of-existence (indeed, ¬P is defined in type theory as P -> false). I think also if I got my types right, you can do
For a constructive proof of ¬¬¬P => ¬P? So it's really just ¬¬P => P that causes trouble. It's not clear to me though whether LEM is actually okay in the "fast and loose reasoning is morally correct" sense (i.e. if it's okay as long as you don't cheat and do something like use a non-terminating loop or exception to make your ¬¬P) though? Are there cases where you didn't "obviously" cheat where it's "wrong" to use it? In some sense, I see ¬¬P => P as a "cast" from an opaque function type to specifically be a closure {f => f(p)} for some p: P.
Partially answering my own question: a starting point for synthetic differential geometry is to define a collection of infinitesimals which aren't not zero, but also zero isn't the only infinitesimal. So there are interesting/non-contrived objects that live in the space left by not assuming LEM.
that isn’t proof by contradiction, that’s plain old proof of negation. proof of negation is Neg : (P → False) → ¬P, proof by contradiction is PBC : (¬P → False) → P. a subtle yet crucial distinction
Ah, right. It's been long enough that I've forgotten what the words mean, though I think with my ninja edit, PBC is actually still constructively valid as a method to prove a negation?
This is needlessly aggressive. There’s nothing wrong with assuming the axiom of choice to prove a result, just as there’s nothing wrong with trying to develop a proof that doesn’t rely on it. Saying a nonconstructive proof “explains nothing” is myopic at best. Insights come from everywhere.
> They're also generally much easier to create than constructive proofs
This is generally seen as a good thing by most "lazy academics". I guess your priorities are just different.
Constructivism is also not at all opposed to infinite quantities (that would be finitism). "Real-world domains" deal with infinite spaces (e.g. of all functions R->R) quite regularly across all scientific domains.
My priorities are indeed different. Apologies for the inflammatory language. My remark WRT constructive proofs is more an observation I've made that most proofs which deal with non-finite values seem to be non-constructive. Not necessarily as a hard and fast rule, the two just don't seem to roll well together. Could be sampling bias, but poking and prodding with mathematician friends more or less confirmed it. Not well read enough to have more interesting things to say on it.
There's a book that's explicitly about this, "Program = Proof", and though it's not beginner and needs maybe a light version for earlier learners, is an excellent example.
I took Haskell in the same uni course with FSAs, Turing machines, Lambda calculus, natural deduction, Hoare logic and structural induction.
So I was exposed to the "base case & step case" of structural induction at the same time as learning about it in Haskell. And for whatever reason it didn't leave a good impression. Implementing it formally was harder and more error-prone than shooting from the hip in an IDE. What was the point!?
Now I smash out Haskell code daily as the quickest way to end up with little binaries that just-work-if-they-compile. It took me a while to realise that the upside of all this formality/mathiness is that other people did the proofs so that you don't need to. I get to take for granted that a boolean is true or false (and not some brilliant third value!) and that (map f . map g) is (map (f . g)).
but in Haskell, yes, it's undefined. Which isn't a real value! For example, infinite loops are undefined. Theorists like to call it a value of every type, but in practical terms, it's more like a computation that never produces a value. The builtin "undefined" can be written as an infinite loop ("undefined = undefined") and many other pure infinite loops can also act as undefined values. The runtime is able to catch some and crash instead of hanging, but not others.
“A man is rich in proportion to the number of things which he can afford to let alone.”
― Henry David Thoreau, Walden or, Life in the Woods
If I have to do the proof, then as you say, it's probably harder and (in many cases) more error prone than if I didn't use a proof. If the language can do it for me, and I get the lack of errors without having to do the work (and without the chance of me making the mistakes)? Yeah, now we're talking. (Yes, I am aware that I still have to design the types to fit what the program is doing.)
If a tool introduces complexity, it has to make up for it by eliminating at least that much complexity somewhere else. If it doesn't, the tool isn't worth using.
Many students do not feel comfortable with proof-writing and cannot dispatch/discharge implications or quantifications correctly when writing proofs and I believe that a structured approach using the Curry-Howard correspondence could help.
[0]: https://en.wikipedia.org/wiki/Curry%E2%80%93Howard_correspon...