👁 Viewing this thread: PracticalPurr, TypeNihilistKat, FormalFelinae, anon-tabby-9182, MewnixOS, and 14 guests
🔥 Dependent Types Are Not the Answer to Everything (fight me)
🔥 HOT 💬 412 replies 👁 18,443 views 📌 Pinned by mods Started by PracticalPurr · 2025-11-03 · Last reply 47 minutes ago
#1
Quote Report

Okay, I'll say it. Dependent types are not a silver bullet and the cult around them is getting exhausting.

I've been writing Lean 4 on and off for about a year now for a verified network protocol stack at $dayjob. I think dependent types are genuinely remarkable for certain problems — statically sized vectors, protocol state machines, resource tracking. But every time I go to a conference or hop on here, someone is evangelising full dependent types as the solution to every software correctness problem, and it just... isn't true.

Here's my actual take: the proof burden for realistic software is enormous, and it scales badly. Let me give a concrete example. In Lean 4, something as simple as proving that a map over a filtered list has the right length requires you to write or invoke lemmas about List.length_filter, List.length_map, and their interactions. That's before you even get to your domain logic.

-- Something that looks "simple" in Lean 4
theorem mapFilter_length (p : α → Bool) (f : α → β) (l : List α) :
    (l.filter p |>.map f).length = (l.filter p).length := by
  simp [List.length_map]  -- okay this one's easy
  -- but now prove the bound you actually care about...
  -- and hope simp closes it. It often doesn't.

For most industry code, what you actually want is refinement types — something like Liquid Haskell where an SMT solver handles the arithmetic automatically, or F*'s hybrid approach. You get 80% of the safety guarantees for maybe 5% of the proof effort.

The Curry–Howard correspondence is beautiful. I'm not denying that. But "proofs are programs" doesn't mean "everyone should write proofs for every program." There's a reason most of the CompCert and seL4 work involves entire teams of verification experts working for years on relatively small codebases.

Fight me in the replies. I have a lot of time on my hands and several stongly-held opinions.

~ "If your type signature takes longer to write than your function, something has gone wrong" ~
#2
Quote Report

You're not wrong but you're framing it as a competition when it isn't one.

Refinement types and dependent types occupy different points on the expressiveness/automation trade-off. LiquidHaskell can verify a ton of arithmetic properties automatically by outsourcing the proof obligation to an SMT solver like Z3 — but the moment you step outside the decidable fragment of the logic, you're stuck. Dependent types let you escape that cage at the cost of writing the proof yourself.

The right analogy: refinement types are like a really smart linter that checks your preconditions automatically. Dependent types are like a proof assistant that can verify anything, if you're willing to do the work.

PracticalPurr wrote:
For most industry code, what you actually want is refinement types...

I broadly agree for most industry code. But "most industry code" isn't what dependent type research is optimising for. The point of Lean 4 Mathlib isn't to write fast CRUD apps — it's to formalise significant chunks of mathematics.

That said I'll concede your SMT point hard. For anything that lives in linear arithmetic, Int bounds checking, list length constraints — an SMT-backed system absolutely annihilates the manual proof approach ergonomics-wise.

#3
Quote Report

Counterpoint: you're complaining about ergonomics, not expressiveness. Those are very different problems.

The simp tactic in Lean 4 is dramatically better than it was in Lean 3. The omega tactic handles most linear arithmetic automatically. The fact that you're writing manual lemmas about List.length_filter suggests you haven't found the right library lemmas, not that dependent types are fundamentally broken:

-- Lean 4 with omega: much less painful
theorem safe_index (v : Vector α n) (i : Fin n) :
    v[i.val]'(i.isLt) = v.get i := by
  simp [Vector.get, Vector.getElem]

-- No manual proof needed for most arithmetic
example (n m : Nat) (h : n < m) : n + 1 ≤ m := by omega

The tooling story for Lean 4 has improved enormously. The aesop, decide, norm_num, and omega tactics together close a huge fraction of "obvious" proof goals automatically. Compare that to Lean 3 or Coq five years ago — it's not even the same experience.

Your seL4 point is fair though. Verification of a full OS microkernel is not something you do casually. But that's an argument for resourcing and tooling maturity, not against dependent types as a concept.

-- FormalFelinae | she/her | "A proof is a program, a program is a proof" --
#4
Quote Report

I want to steelman OP's position a bit more aggressively than the previous replies have.

The thing people miss is that proof burden scales superlinearly with specification complexity. Adding a simple size constraint to your type is easy. Adding a sortedness invariant is harder. Adding a sorted, size-bounded, de-duplicated list with provably correct insert and delete is now a small research project. Composing multiple such invariants together is where things really go sideways.

There's also the issue of what I call proof brittleness. In Idris 2, a seemingly innocuous refactoring of your data structure can cascade into hours of proof repair. You didn't change the semantics at all — you just reorganised the fields of a record — and now rewrite chains don't work anymore and you have to figure out why Refl is unhappy.

-- Idris 2: what should be a trivial equality
-- becomes a painful exercise in rewriting
data SortedVec : (n : Nat) → Type where
  Nil  : SortedVec 0
  Cons : (x : Int) → SortedVec n
       → {auto ok : headGe x sv} → SortedVec (n + 1)
-- Now try to prove concatenation preserves sortedness.
-- I'll wait.

I've seen senior engineers with ML PhDs bounce off Idris 2 because the implicit argument resolution is finicky — as someone pointed out elsewhere, even a good tactic can require you to supply a bunch of additional arguments explicitly because the elaborator can't figure out what you mean.

The ergonomics problem IS the problem. Beautiful theory that nobody can use in practice is just philosophy.

#5
Quote Report

Hot take incoming: the real problem isn't dependent types vs refinement types — it's that we keep trying to find one solution when the answer is obviously stratified verification.

Think about it as a pyramid. At the bottom you have your ordinary types (Rust's borrow checker, Go's nil safety, whatever). One layer up, refinement types with SMT discharge handle arithmetic and shape constraints with near-zero proof overhead. At the top, for the 5% of code that is truly safety-critical and structurally complex, you break out full dependent types.

This is basically what F* does! You have SMT-dischargeable VCs for most things and you can drop into manual proofs when you need them. Steel and Pulse for separation logic, full dependent types for the gnarly stuff. The Tot / Ghost / Lemma / ST effect layering is actually a really elegant answer to the "how much verification do you want" question.

(* F* effect system lets you choose your verification budget *)
val safe_div : x:int → y:int{y <> 0} → Tot int
let safe_div x y = x / y
(* SMT handles the precondition automatically at callsites *)
(* No manual proof lemma required *)

The frustrating thing is that F* is still kind of a niche tool and most people in industry don't know it exists. Meanwhile everyone's trying to use Lean 4 for everything because Mathlib is impressive and it has good marketing.

-- "The types are trying to tell you something. Maybe listen." --
#6
Quote Report
FormalFelinae wrote:
you're complaining about ergonomics, not expressiveness. Those are very different problems.

No, I'm explicitly saying ergonomics is the problem for practical use. A system that is theoretically complete but practically unusable for the median team is not a win — it's a research prototype.

I'll grant you that Lean 4's omega and simp have gotten much better. I use them daily. But I maintain that there's a category of problems — think: "verify that this cryptographic state machine never enters an invalid state under concurrent access" — where even the best tactics leave you drowning in proof obligations. The specification gap between what you want to say and what the type theory lets you say automatically is still enormous.

LambdaCatgirl wrote:
everyone's trying to use Lean 4 for everything because Mathlib is impressive and it has good marketing.

THIS. Lean 4 has incredible momentum right now because of the Mathlib4 port and some very good YouTube content (shoutout to the FRO folks). But Mathlib momentum ≠ "best tool for your verification problem." F* and its SMT integration is genuinely more ergonomic for a lot of software verification tasks precisely because it doesn't ask you to construct proof terms for things a Z3 call could handle in 0.1 seconds.

Although I'll say, the Decidable machinery in Lean 4 plus decide is kind of magic when your domain is finite enough to enumerate.

#7
Quote Report

Coming at this from a research angle: I think the original post conflates "dependent types are not the answer to everything" (true) with "dependent types are not worth the effort" (false for the domains they're designed for).

In my thesis work (verified compilation of a DSL for distributed systems), full dependent types in Agda let me prove properties that would be literally impossible to express in a refinement type system — for example, proving that two separately compiled modules, when linked, preserve a global invariant about message ordering. The types need to talk about the relationship between program structure and semantic behaviour, which requires computation in types.

Refinement types are restricted dependent types. A refinement type { x : A | P x } is literally a Σ-type where the predicate is restricted to a decidable fragment. The restriction buys you automation. That's a great trade if your problem lives in that fragment!

But my problems don't, and I suspect a lot of critical infrastructure verification problems don't either. seL4 wasn't verified using LiquidC for a reason.

#8
Quote Report

Can we talk about the universe polymorphism situation for a moment because nobody mentions this when they're evangelising dependent types to beginners.

In Lean 4, Agda, and Coq, you have this whole universe hierarchy to prevent Russell-style paradoxes. Type 0 : Type 1 : Type 2 : ... etc. For most code it's invisible. For library-level generic code — the kind where you're abstracting over types of types — it becomes a genuine headache. You start getting universe constraint errors that are almost impossible to debug without a deep understanding of the metatheory.

-- Lean 4: universe polymorphism becoming visible
universe u v
def myFunctor.{u, v} (F : Type u → Type v) : Type (max u v + 1) := ...
-- "Why is the universe max+1??" — every Lean 4 newcomer

This is a solvable problem with experience but it represents a genuine onboarding cliff. When I'm trying to convince my employer to adopt a verification tool, "it requires deep knowledge of Martin-Löf type theory to write generic library code" is a tough sell.

Agda at least has universe polymorphism that's slightly more transparent in my experience, but it's still a lot.

#9
Quote Report

Hard SMT enjoyer checking in. I want to make a point about decidability that I think gets glossed over.

The reason refinement type systems backed by SMT are so ergonomically pleasant is that the verification is fully automated within their fragment. You write the spec, the checker runs, you get a yes/no answer with no interaction required. This is what LiquidHaskell gives you for most real-world Haskell properties — the paper showed it could prove 96% of recursive functions terminating with roughly 1.7 lines of termination annotations per 100 lines of code. That's an incredible ratio.

With dependent types, even with great tactics, you're in an interactive loop. You write a partial proof, inspect the goal state, apply a tactic, inspect again. It's powerful and I respect it, but let's not pretend it's the same workflow. For a team that needs to verify things and ship code, the interactive proof loop has real costs.

GradCatStudent wrote:
seL4 wasn't verified using LiquidC for a reason.

True. And ALSO the seL4 proofs took approximately 11 person-years of expert verification effort for roughly 10,000 lines of C. That's a data point about cost, not just capability. I love seL4. I would not recommend the seL4 approach for a fintech startup's authentication module.

-- SMT: because some of us have deadlines --
#10
Quote Report

Okay I want to actually steelman the OP's case here because I've been pushing back and I think there's a real insight buried in it that we haven't quite named.

The core problem with full dependent types for most teams is what I'll call the specification oracle problem. In order to use dependent types effectively, you need to know what property you want to prove before you write the code. The type IS the specification. But in industry, specifications are often discovered iteratively — you write the code, run it, find the bugs, refine your understanding of what correctness means.

Dependent types as a workflow assume a waterfall-ish view of development: spec first, code second. Refinement types (and especially tools like LiquidHaskell applied to existing codebases) fit much better into an agile, iterative workflow because you can add refinements after the fact and they integrate into existing code without disruption.

This is actually one of LiquidHaskell's design goals — it was designed to be an optional checker that doesn't affect the dynamic semantics, making it easier to apply to existing libraries. You can add it incrementally. You can't really do that with full dependent types — you're essentially committed to a different programming paradigm from the start.

So OP: I'm not going to say you're wrong. I'm going to say that for most teams, most of the time, you're correct. And for the research/critical-infrastructure cases where full DT is the right answer, the right move is to hire people who know what they're doing. This doesn't have to be a universal truth.

-- FormalFelinae | she/her | "A proof is a program, a program is a proof" --
#11
Quote Report

Real talk: we are also sleeping on GADTs as a middle ground that nobody talks about in this debate.

GADTs in Haskell or OCaml give you a significant chunk of dependent type expressiveness — phantom type parameters that track runtime-relevant structure, existential types, index-enforced state machines — without requiring you to commit to a full proof assistant. Most type-state patterns that people reach for dependent types to implement can be handled with GADTs + smart constructors + a bit of newtype discipline.

-- Haskell: type-state with GADTs, no proof terms needed
data Connection (s :: State) where
  Closed :: Connection 'Closed
  Open   :: Handle → Connection 'Open

send :: Connection 'OpenByteStringIO ()
close :: Connection 'OpenIO (Connection 'Closed)

This is expressible, readable, and requires zero familiarity with Martin-Löf type theory. A junior Haskell dev can understand this type-state pattern in an afternoon. Getting to the same place with Idris 2 indexed types would require a week of ramp-up minimum.

The ladder of verification commitment goes: untyped → simple types → GADTs/phantom types → refinement types → full dependent types. Most industrial code has no business being above step 4.

#12
Quote Report

I want to bring up something nobody has mentioned: proof maintenance cost.

Even if you successfully verify a module with full dependent types, you're creating a maintenance liability. Every time the API changes, every time the data structure evolves, every time a business requirement shifts the invariant — your proofs break. And not in a way that's easy to fix; it's not like a type error where the compiler tells you exactly what to update. Broken proofs can be deeply confusing to fix, especially if the original author has left the team.

This is an under-discussed cost in the academic literature, which tends to present verification as a one-time effort ("we verified X!") rather than an ongoing engineering commitment. The Mathlib project actually does a lot of work maintaining proofs across Lean version upgrades and refactors — but Mathlib has dedicated contributors and institutional support. Most industry teams don't have that.

FormalFelinae wrote:
for most teams, most of the time, you're correct.

I'm starting to believe this more than I'd like to admit as someone who spent three months last year doing nothing but proof repair after a library refactor. It's not a fun job.

That said: I still think for protocol implementations, cryptographic primitives, and OS-level code, the investment pays off. The question is whether your codebase is more like seL4 or more like a typical web backend. If it's the latter, please consider just writing good tests and using property-based testing before you commit your team to Idris 2.

#13
Quote Report

This thread is going better than I expected. Let me try to summarise the emerging consensus before we head to page 2:

Things we seem to agree on:

① Dependent types and refinement types are not competitors — they're at different points on an expressiveness/automation trade-off curve.
② For most industry code, SMT-backed refinement types give better ROI on verification effort.
③ Full dependent types are the right tool for: OS-level code, cryptographic protocols, formal mathematics, and other domains where you need to prove properties beyond decidable arithmetic.
④ Lean 4's ergonomics have improved dramatically but there's still a steep learning curve for non-trivial proofs.
⑤ Proof maintenance cost is a real, underappreciated burden.

Things we're still fighting about:

① Whether GADTs + refinement types are "good enough" for the 95th percentile of use cases (I say yes, @GradCatStudent says not for their work).
② Whether Lean 4 vs F* is the right choice for the hybrid SMT+DT approach.
③ Whether it's the tooling or the theory that's limiting adoption.

Continuing this on page 2. @WitnessKitty I saw your draft reply about observational type theory and I am HERE for it, please post it.

~ "If your type signature takes longer to write than your function, something has gone wrong" ~