-
Notifications
You must be signed in to change notification settings - Fork 4
0111: Email Tutorials ‐ Type‐Based Formal Verification in FP
Type-Based Formal Verification in FP
- Email 900 – What “Formal Verification” Means
- Email 901 – Where Types Fit Into Formal Verification
- Email 902 – Quick Haskell Background You Need
- Email 903 – The Problem: Types Sometimes Lie
- Email 904 – Two Classic Fixes and Why They’re Not Perfect
- Email 905 – Strengthening Types With GADTs
- Email 906 – Why GADTs Are Useful, and Their Costs
- Email 907 – The “Replicate” Problem
- Email 908 – Dependent Types
- Email 909 – What Dependent Types Give You
- Email 910 – Why Dependent Types Are Hard in Practice
- Email 911 – Refinement Types
- Email 912 – What Refinement Types Look Like
- Email 913 – What Happens With Real-World Inputs
- Email 914 – Higher-Order Functions Are Hard to Specify
- Email 915 – Intrinsic vs Extrinsic Verification
- Email 916 – Proofs and Why Compilers Ask for Them
- Email 917 – Proof Automation: Tactics and SMT Solvers
- Email 918 – Why People Don’t Use This Everywhere Yet
- Email 919 – Summary
- Glossary
- Quiz: 20 Questions + Options
- Answers + Explanations
Formal verification is the idea that you have two things:
- A program (the thing you run).
- A specification (the thing you want the program to satisfy).
Then you try to prove or check that the program always satisfies the specification.
A specification can be very small or very big. It might say, “this function never divides by zero,” or it might say, “this whole system never reaches an inconsistent state.”
You can do verification at different times. You can model early (before writing code), verify while coding, or write code first and verify later.
Types are often called a lightweight formal verification tool, because the compiler checks a lot for you.
A type system already prevents many mistakes. For example, if the compiler knows something is a String, it will complain if you try to treat it like an Int. That is a form of correctness checking.
But the talk’s main point is this: basic types don’t express enough invariants.
Knowing something is an Int is helpful, but it doesn’t tell you “this Int is not zero,” or “this list is not empty,” or “this list has exactly n elements.”
Type-based formal verification tries to push the type system further, so the compiler can check stronger correctness properties.
The talk uses Haskell-like syntax, so here are the essentials.
A classic Haskell list is defined as:
- Either the empty list (
Nil) - Or a head element plus a tail list (
Cons x xs)
Functions can be written using pattern matching. For example, length can be defined by matching on the list constructor.
Haskell also usually writes type signatures separately from function implementations. So you may see:
length :: List a -> Int
This means “length takes a list of any element type a and returns an Int.”
This is one of the most important beginner points.
A type signature can claim that a function is “safe,” while the function can still crash at runtime.
For example:
head :: [a] -> a
This looks like it always succeeds, but it fails on [].
Or:
div :: Int -> Int -> Int
This looks safe, but fails when the second argument is 0.
So even in a strongly typed language, you can still write programs that crash. That is why the speaker says: types are great, but sometimes types lie.
When a function can fail, developers usually do one of these:
You say: “head only accepts non-empty lists” or “div only accepts non-zero denominators.”
That sounds good, but standard types don’t naturally express “non-empty” or “non-zero” without extra tools.
You change the type to return an error-aware type:
safeHead :: [a] -> Maybe asafeDiv :: Int -> Int -> Either DivByZeroError Int
This works, but now you must pattern match everywhere. Even if you already know the number is non-zero, the type forces you to handle error cases again and again. This can clutter the “happy path.”
The deeper goal is: the compiler should learn from earlier checks, so you don’t keep re-checking forever.
GADTs let you encode extra information inside the type.
The talk’s key example is: a list type that “remembers” its length.
Instead of:
- “this is a list of
a”
you get:
- “this is a list of
awith lengthn”
Then you can define head so the type forces the list to be non-empty, because its length must be at least 1.
The big win is that the compiler can reject invalid programs at compile time, not runtime.
GADTs are powerful, but they come with costs:
- You may need custom number types at the type level (like
Zero,Succ n). - You may need custom list types instead of standard lists.
- You must decide in advance what property you want tracked (length, parity, sortedness, etc.).
So GADTs are useful when you know exactly what invariant you want and can afford specialized structures.
The talk highlights a big limitation.
A function like:
replicate n x
creates a list whose length depends on a runtime value n.
With just GADTs (in the style shown), it’s hard to express “the output list length equals the runtime number n,” because n lives at runtime but the length index lives at compile-time type level.
This pushes us toward dependent types or refinement types.
Dependent types let the type of a result depend on the value of an input.
That means you can write something like:
replicate : (n : Nat) -> a -> Vec a n
Here, the type Vec a n literally contains the value n.
This “blurs” the line between types and values. In many dependent type languages, types and values live in one unified world.
Once values can appear in types, you can express very strong facts:
- Appending
Vec a mandVec a ngivesVec a (m + n) -
replicate n xproduces a vector of lengthn
You can also use functions in types (like m + n), because + is just a function and types can contain values.
The talk warns about real-world pain points:
- Type checking may require evaluation, meaning compilation may need to run more computation.
- Type inference becomes difficult, so you often write more annotations.
- Proof obligations appear: the compiler may ask you to prove facts you consider “obvious.”
This is why dependent type systems can feel like programming plus doing math proofs.
Refinement types take another route.
Instead of turning runtime values into type indices, you keep your normal types but add logical predicates.
So you can express:
- “an
Intwherex ≠ 0” - “a list where
length xs > 0”
This is often done in systems like Liquid Haskell.
A refinement type looks like:
x:Int | x /= 0
or for lists:
xs:[a] | length xs > 0
In Liquid Haskell style, you add annotations that specify these constraints, and a solver tries to prove your program respects them.
This makes it possible to keep using normal Haskell datatypes while still expressing stronger safety properties.
A beginner-friendly way to think about it:
If your denominator comes from the internet, the compiler cannot assume it is non-zero.
So you still need runtime checks.
But once you check and branch:
- If
d == 0, handle error. - Else, in the
elsebranch, the system can treatdas non-zero.
That means the compiler learns facts from control flow and allows safe functions without repeated error-handling clutter.
This is one of the most important warnings.
Specifying properties of functions like map can be tricky because:
- You may care about length preservation.
- Or about preserving ordering.
- Or about mapping a predicate
Pto a predicateQ. - Or about algebraic laws like
map id == id.
Different use cases want different specifications. Writing the “best” type for higher-order functions becomes complicated.
This is why some verified codebases sometimes avoid generic functions and instead write explicit recursion for clarity and provability.
The talk distinguishes two styles:
You bake correctness into the types so incorrect programs do not type-check.
You write a normal program first, then separately write proofs/theorems about it.
Some languages lean toward one style more than the other, and modeling choices can determine whether verification is easy or painful.
In dependent type systems, sometimes the compiler asks you to prove things like:
- “
n + 1equalsSucc nin some representation”
These feel trivial to humans but are not obvious to the compiler unless the system is designed to rewrite and simplify automatically.
This is where you start writing proof code, often guided by the Curry–Howard idea: types are propositions, programs are proofs.
The talk names three “styles”:
- Do proofs by hand (some systems basically force this).
- Use tactics (automation scripts for common proof steps).
- Use SMT solvers (like Z3) to discharge routine arithmetic/logical obligations automatically.
Refinement-type tools often rely heavily on SMT solvers to reduce manual proof burden.
The speaker gives practical reasons:
- Tools can fail unpredictably (“it doesn’t prove it, and you don’t know why”).
- Modeling is still exploratory; the same property may be easy in one encoding and hard in another.
- There is a learning curve: you must learn proof thinking and solver limitations.
So the “best” current entry point is often refinement types in familiar languages, because it adds power with less disruption.
Type-based formal verification is about using stronger type systems to make correctness properties checkable by the compiler.
The talk presented three major families:
- GADTs: encode invariants directly into types (like list length indices).
- Dependent types: allow types to depend on values, enabling precise specs like “replicate returns length n.”
- Refinement types: keep normal types but add logical predicates checked by solvers.
Each approach increases correctness guarantees but also increases complexity, annotation burden, or proof obligations.
Formal verification – Proving/checking that a program satisfies a specification, often for all possible inputs.
Specification – A precise statement of what correctness means (e.g., “never divides by zero,” “result is sorted,” “length preserved”).
Type system – A system that assigns types to values and checks whether programs use values consistently.
Static typing – Type errors are caught at compile time.
Runtime error – A failure that happens while the program runs (like exceptions from head []).
Invariant – A property that should always hold (e.g., “list is non-empty,” “denominator is non-zero”).
Maybe / Option – A type representing “a value may be missing” (Nothing or Just x).
Either – A type representing success or failure (Left err or Right value).
Sum type – A type with multiple constructors (e.g., User = PersonUser Person | EntityUser Entity).
Pattern matching – Selecting behavior based on which constructor a value has.
GADT (Generalized Algebraic Data Type) – A data type where constructors can refine the resulting type, letting you encode invariants in types.
Type index – Extra type parameters used to represent properties (like the length of a list).
Dependent type – A type that can depend on a runtime value, allowing very precise specifications.
Vector (Vec) – A length-indexed list, common in dependent type examples.
Refinement type – A type annotated with a predicate (e.g., Int where x != 0).
Predicate – A boolean condition about a value (e.g., length xs > 0).
Precondition – A condition that must be true before calling a function.
Postcondition – A condition that must be true after a function returns.
SMT solver – A tool that tries to determine if logical constraints are satisfiable; used to automatically prove many small obligations.
Z3 – A popular SMT solver used by many verification tools.
Curry–Howard correspondence – The idea that types correspond to logical propositions and programs correspond to proofs.
Proof obligation – A required proof step the system demands to accept your program.
Tactic – An automated proof strategy that fills in common proof steps.
Intrinsic verification – Correctness is enforced by types; invalid states are unrepresentable.
Extrinsic verification – Write code first, then write separate proofs about it.
A. Testing a program with random inputs B. Proving/checking a program satisfies a specification C. Making code run faster D. Removing all types from code
A. They guarantee perfect security B. They prevent all runtime errors C. They enforce some correctness constraints automatically D. They replace documentation
A. Because it returns Nothing
B. Because it fails on empty lists even though the type doesn’t show failure
C. Because it always returns the last element
D. Because it is polymorphic
A. safeHead :: [a] -> Maybe a
B. safeHead :: [a] -> a
C. safeHead :: a -> [a]
D. safeHead :: Int -> a
A. It makes programs impossible to compile B. It forces repeated pattern matching and can clutter the happy path C. It removes static typing D. It prevents recursion
A. Remove types entirely B. Encode extra invariants in the type itself C. Replace all functions with macros D. Make all lists mutable
A. The first element B. The memory address C. The length of the list D. The sorting order only
A. Because replicate has no recursion
B. Because the output length depends on a runtime value n
C. Because lists cannot store values
D. Because Haskell has no numbers
A. A type that depends on another type only B. A type that depends on runtime values C. A type that removes recursion D. A type that only exists at runtime
A. Are totally separate worlds B. Cannot mention each other C. Live in a unified world where values can appear in types D. Are replaced by comments
A. They forbid compilation B. Type checking may require running computations and more annotations C. They remove pattern matching D. They eliminate proof obligations completely
A. A type rewritten in assembly
B. A type plus a predicate/constraint about values
C. A type that forbids any predicates
D. A type that always equals Any
A. It ensures the value is always negative B. It ensures the value is not zero C. It ensures the value is a string D. It ensures the value is prime
A. CSS preprocessors B. SMT solvers (e.g., Z3) C. JPEG codecs D. GPU shaders
A. Because the internet is slow B. Because input is unknown at compile time C. Because refinements only work for strings D. Because refinements forbid branching
16) After checking if d == 0 then ... else ..., what can refinement systems often do in the else branch?
A. Forget what happened
B. Treat d as non-zero inside that branch
C. Turn d into a list
D. Make d polymorphic
A. Because they cannot be executed B. Because there are many meaningful properties one might want to express C. Because they have no types D. Because they only exist in Scala
A. Verify after deployment only B. Encode correctness in types so invalid programs don’t type-check C. Use only unit tests D. Ignore invariants
A. Encode everything in types only B. Write code, then separately prove properties about it C. Only verify performance D. Verification without any specification
A. It is illegal B. Tools are always perfect C. Tools can fail opaquely and modeling has a learning curve D. It always makes code slower at runtime
- B – Formal verification is about checking/proving code satisfies a spec.
- C – Types prevent certain classes of errors automatically.
-
B –
headcan crash on[], but the type doesn’t show failure. -
A –
Maybe aexpresses possible failure without exceptions. - B – You often re-check impossible cases and clutter the happy path.
- B – GADTs let types carry extra correctness information.
- C – The length becomes part of the type.
-
B –
nis runtime, but the length index is checked statically. - B – Dependent types allow result types to depend on values.
- C – Values can appear in types in dependent type systems.
- B – More computation during type checking + more annotations + proofs.
- B – A refinement type is a normal type plus a predicate.
- B – It guarantees the integer is non-zero.
- B – SMT solvers automate many boring proof obligations.
- B – Inputs aren’t known at compile time; you must validate.
-
B – The system can learn
d != 0in theelsebranch. - B – There are many valid specs: length, ordering, laws, predicates, etc.
- B – Intrinsic means correctness-by-construction in the type system.
- B – Extrinsic means code + separate proofs about it.
- C – Tools can fail with unclear reasons; modeling requires skill.
Bernard Sibanda is a global Technology Entrepreneur, Web3 and Software Consultant with a deep focus on Cardano Blockchain, Midnight and Community building.
Key Positions:
- Founder, CTO, Developer Advocate cohort #1, Fullstake Developer, Cardano Ambassador, Catalyst Project Manager, DREP-WIMS:
- Co-founder of ABL Tech and Cardano Africa Live
- EBU-certified Plutus Pioneer (Plutus/Haskell)
- Cohort #1 Plutus Pioneer Developer
- Catalyst Community Reviewer & Funded Projects Manager
-
DRep for WIMS-Cardano (ID:
drep1yguj8zu48n99pv70yl6ckzt9hdgjy8yjnlqs2uyzcpafnjgu4vkul) - Intersect Developer Advocate
- Intersect Committe Member 2025-2026
- Cardano Marketer,Promoter and blogger
- Cardano Open Source Contributor
- Cardano communities and events organizer and builder
- Cardano Ambassador for South Africa
Official links:
- Stablecoins Dex
- Coxygen Global Universities
- WIMS Cardano Global
- Cardano Africa Live
- WIMS Cardano Videos
- Cardano Smart Contract Videos
- Fullstack IT Consulting
Social links: