Formalization

Lean 4 · Berkeley DRP · Fall 2025

For all V in G, the preimage of V under f is in F One definition of a limit, in place of about 169

Written out with epsilons and deltas, "tends to" is not one idea but roughly 169 of them, one for each way the input can approach something and each way the output can. Filters collapse the list. This is a reading project on how, with the definitions and proofs checked in Lean.

project.lean composition of limits
example {X Y Z : Type*} {F : Filter X} {G : Filter Y} {H : Filter Z}
    {f : X → Y} {g : Y → Z}
    (hf : Tendsto f F G) (hg : Tendsto g G H) :
    Tendsto (g ∘ f) F H := by
    refine (tendsto_def.mpr ?_)
    intro s hs
    have hG : g ⁻¹' s ∈ G := (tendsto_def.mp hg) s hs
    have hF : f ⁻¹' (g ⁻¹' s) ∈ F := (tendsto_def.mp hf) _ hG
    simpa [Set.preimage, Function.comp] using hF
169
Limit definitions, replaced by one
2,197
Composition lemmas, replaced by one
7
Results checked in Lean
106
Lines, one file

In plain terms

A first course in analysis defines a limit with epsilons and deltas: for every tolerance you name, there is a point past which the values stay inside it. That works on paper because the reader silently fills in the variations — a limit taken from the left, a limit running off to infinity, a limit along a sequence — without anyone writing them out. A computer fills in nothing. Written out in full there are about 169 separate definitions of "tends to", one for each pairing of how the input approaches something and how the output does, and roughly 2,197 further results needed just to chain two of them together. Filters are a way of saying "eventually" and "close enough" without ever naming an epsilon, and they replace the whole list with one definition. This was a semester in Berkeley's Directed Reading Program, working with a graduate mentor: read the theory, restate the basic results of real analysis in that language, and check them in Lean 4, which verifies each step mechanically. The result is a single file of 106 lines covering convergence, continuity, composition and a few standard consequences.

01 The 169

"Tends to" is not one definition. Written out, it is about 169.

A first analysis course defines a limit with epsilons. A sequence converges to L when, for every ε > 0, there is an N past which every term sits within ε of L. A function is continuous at x0 when, for every ε, there is a δ that keeps the output within ε.

That works on paper because a reader fills in the variations. Nobody writes out the definition of a one-sided limit at a point where the function runs off to infinity; they write one case and say the others go the same way. A proof assistant does not fill anything in. Every case has to be stated.

Count them. There are roughly thirteen ways the input can approach something: toward a point, toward a point from the left, from the right, avoiding the point, out to +∞, out to −∞, along a sequence, and so on. Roughly thirteen for the output too. That is 169 separate statements, each needing its own definition.

Composition is worse. If f tends one way and g tends another, what does g∘f do? Three slots, thirteen choices each: about 2,197 lemmas. The figures come from a Lean workshop talk that this project's paper takes as its starting point.

13 times 13 equals 169 definitions; 13 cubed equals 2197 composition lemmas.

Every cell here is a different definition

The grid below is a representative thirteen by thirteen. Pick any cell and it prints the epsilon-and-delta statement you would have to write for that combination. All 169 of them are the same statement in the filter language, shown underneath.

how the input approaches → what the output approaches →

Change the cell and the first line changes. The second line does not.

02 What a filter is

Three conditions

A way to say "eventually" without saying when

A filter on a set X is a collection of subsets of X, thought of as the sets that are "big enough" for whatever purpose is at hand. It has to satisfy three conditions.

  1. The empty set is not in it

    Otherwise "big enough" would include nothing at all.

  2. Anything bigger is also in it

    If A is big enough and A ⊆ B, then B is big enough.

  3. Two of them overlap in one

    If A and B are both big enough, so is A ∩ B. This is what lets you combine two conditions, the way N = max{N1, N2} does in an epsilon proof.

The empty set is not in F; F is closed under supersets; F is closed under finite intersections.

Three that come up constantly

The neighbourhood filter at a point x: all sets containing an open interval around x. This is "sufficiently close".

The neighbourhood filter at x is the collection of subsets of the reals containing an interval around x.

The tail filter at +∞: all sets containing a ray. This is "sufficiently large".

The filter at plus infinity is the collection of subsets of the reals containing a ray from A to infinity.

The principal filter on a set A: everything containing A. On X = {1,2,3,4} with A = {3,4} that is four sets.

The principal filter on the set three four consists of four subsets.

Try building one

Below is every subset of {1,2,3,4}. Switch some on and the panel checks the three conditions against your collection, and names the pair that breaks whichever one fails.

Start from

    03 Convergence

    Sequences first

    The same convergence, said with sets

    Before the general definition, the special case. Take the collection of subsets of ℕ whose complement is finite — the cofinite, or Fréchet, filter.

    F sub r is the collection of subsets S of the naturals whose complement is finite.

    A set is in it exactly when it contains a tail. That is the first lemma, and it is what makes this filter mean "sufficiently large".

    A is in F sub r if and only if A contains the tail from some nu onward.
    def frechetFilter : Filter ℕ :=
      Filter.cofinite
    
    lemma frechet_tail_lemma (A : Set ℕ) :
      A ∈ frechetFilter ↔ ∃ a : ℕ, Set.Ici a ⊆ A :=by
      have h1 : A ∈ frechetFilter ↔ A ∈ (Filter.atTop : Filter ℕ) := by
        simp [frechetFilter, Nat.cofinite_eq_atTop]
      have h2 : A ∈ (Filter.atTop : Filter ℕ) ↔ ∃ N, ∀ n ≥ N, n ∈ A :=by
        exact (Filter.mem_atTop_sets)
      have h3 : (∃ N, ∀ n ≥ N, n ∈ A) ↔ ∃ N, Set.Ici N ⊆ A := by
        exact Eq.to_iff rfl
      exact (h1.trans h2).trans h3
    Set.Ici a is the set of naturals from a upward, so the statement reads: A is cofinite exactly when it contains some tail.

    Now fix a sequence and a candidate limit, and for each ε collect the indices where the sequence is already within ε.

    S sub epsilon is the set of indices n where the distance from a n to L is less than epsilon.

    Convergence in the usual sense says exactly that every one of those sets is in the filter.

    The epsilon N definition of the limit is equivalent to saying every S sub epsilon lies in the Frechet filter.

    Nothing has been gained yet — this is the same statement twice. What it buys is the change of shape. The left side quantifies over indices; the right side asks whether a set is in a collection. The next section is where that pays.

    lemma seq_filter_equiv
        (a : ℕ → ℝ) (L : ℝ) :
        (∀ ε > (0 : ℝ), ∃ N : ℕ, ∀ n ≥ N, |a n - L| < ε) ↔
        (∀ ε > (0 : ℝ), {n : ℕ | |a n - L| < ε} ∈ frechetFilter) :=by
      constructor
      · intro h ε hε
        rcases h ε hε with ⟨N, hN⟩
        have htail :
            ∃ N, Set.Ici N ⊆ {n : ℕ | |a n - L| < ε} := by
          refine ⟨N, ?_⟩
          intro n hn
          exact hN n hn
        exact (frechet_tail_lemma _).mpr htail
      ·
        intro h ε hε
        have h' :
        ∃ N, Set.Ici N ⊆ {n : ℕ | |a n - L| < ε} := by
          apply (frechet_tail_lemma _).mp
          exact h ε hε
        rcases h' with ⟨N, hN⟩
        refine ⟨N, ?_⟩
        intro n hn
        exact hN hn
    Both directions go through the tail lemma, which is the only real content. Everything else is unpacking.
    04 One definition

    The whole idea

    Drop the specifics and the 169 collapse

    Sequences used two particular filters: the tail filter on ℕ and the neighbourhood filter at L. Leave both unspecified and the definition stops being about sequences.

    For all V in G, the preimage of V under f lies in F.

    X and Y are sets, F is a filter on X, G is a filter on Y, and f maps X to Y. Read it as: whatever counts as big enough in the target, its preimage counts as big enough in the source.

    Every cell of the grid in Section 01 is this statement with a particular F and G filled in. A one-sided limit is a different F. Running off to infinity is a different G. Sequences are a different X. The definition does not change.

    One quantifier instead of three

    The other thing worth noticing is the shape. The epsilon version alternates quantifiers: for all ε, there exists N, for all n. Alternating quantifiers are what make analysis proofs fiddly, because each one has to be introduced and discharged in the right order. The filter version has one.

    For all epsilon, there exists N, for all n — versus for all V.

    Composition, which is where the saving shows up

    Classically this is a lemma about points, and it has a version for every combination of ways the three limits can be taken.

    If f tends to b as x tends to a, and g tends to c as y tends to b, then g of f of x tends to c.

    With filters it is a statement about three filters and two functions, and the proof is to pull a set backwards twice.

    If f goes from F to G and g goes from G to H, then g composed with f goes from F to H.

    Step through it. The set starts in the target and travels back to the source, one preimage at a time.

    X F
    f
    Y G
    g
    Z H

    V in H gives g inverse of V in G, which gives f inverse of g inverse of V in F.
    example {X Y Z : Type*} {F : Filter X} {G : Filter Y} {H : Filter Z}
        {f : X → Y} {g : Y → Z}
        (hf : Tendsto f F G) (hg : Tendsto g G H) :
        Tendsto (g ∘ f) F H := by
        refine (tendsto_def.mpr ?_)
        intro s hs
        have hf' := tendsto_def.mp hf
        have hg' := tendsto_def.mp hg
        have hG : g ⁻¹' s ∈ G := hg' s hs
        have hF : f ⁻¹' (g ⁻¹' s) ∈ F := hf' _ hG
        simpa [Set.preimage, Function.comp] using hF
    Nine lines, and none of them mention a point, a distance, or an epsilon. This is the single lemma standing in for the 2,197.

    A constant sequence converges to its own value. Classically you argue that the difference is 0 for every index. With filters the preimage of any neighbourhood is all of ℕ, and the whole set is in every filter, so there is nothing left to check.

    theorem tendsto_const_seq (c : ℝ) :
        Tendsto (fun _ : ℕ => c) (atTop : Filter ℕ) (𝓝 c) := by
      refine (tendsto_def.mpr ?_)
      intro s hs
      have h': c∈s:=by
        exact mem_of_mem_nhds hs
      have hpre:(fun _ : ℕ => c)⁻¹'s=Set.univ :=by
        ext n
        simp[h']
      simp[hpre]
    Set.univ is the whole space. Upward closure puts it in every filter, which is the second axiom doing the work.
    05 Continuity

    The same definition again

    Continuity is not a new idea

    Take the general definition, put the neighbourhood filter at x in the source slot and the neighbourhood filter at f(x) in the target slot. That is continuity at a point.

    For all V in the neighbourhood filter at f of x, the preimage of V lies in the neighbourhood filter at x.

    Points close enough to x land close enough to f(x). The sequential definition says the same thing along sequences, and the epsilon-delta definition says it with two explicit numbers.

    If x n tends to x zero then f of x n tends to f of x zero.

    The two agree, and in Lean that is one line, because Mathlib already carries the bridge between its topological and metric definitions.

    -- ε–δ definition of continuity at a point x₀ for f : ℝ → ℝ
    def cont_eps_delta (f : ℝ → ℝ) (x₀ : ℝ) : Prop :=
      ∀ ε > 0, ∃ δ > 0, ∀ {x : ℝ}, dist x x₀ < δ → dist (f x) (f x₀) < ε
    
    theorem cont_filter_equiv (f : ℝ → ℝ) (x₀ : ℝ) :
      ContinuousAt f x₀ ↔ cont_eps_delta f x₀ := by
      simpa [cont_eps_delta] using
        (Metric.continuousAt_iff (f := f) : ContinuousAt f x₀ ↔ _)
    The point of stating it is to check that the filter definition being used is the familiar one, not to discover something new.

    A sum of continuous functions

    Classically you pick Nf and Ng for the two functions and take the larger. With filters you build the pair (f, g) as a single map into ℝ × ℝ, observe that addition is continuous, and compose.

    If f and g are continuous at x zero then f plus g is continuous at x zero.
    theorem continuousAt_add
        (f g : ℝ → ℝ) (x : ℝ)
        (hf : ContinuousAt f x)
        (hg : ContinuousAt g x) :
        ContinuousAt (fun y => f y + g y) x := by
      let h : ℝ → ℝ × ℝ := fun y => (f y, g y)
      let k : ℝ × ℝ → ℝ := fun p => p.1 + p.2
      have h_pair : ContinuousAt h x := by
        simpa [h] using hf.prodMk hg
      have h_add : ContinuousAt k (h x) := by
        have hk : Continuous k := by
          simpa [k] using
            (continuous_fst.add continuous_snd :
              Continuous fun p : ℝ × ℝ => p.1 + p.2)
        simpa [h] using hk.continuousAt (x := h x)
      have h_comp : ContinuousAt (fun y => k (h y)) x :=
        ContinuousAt.comp' (f := h) (g := k) (x := x) h_add h_pair
      simpa [h, k] using h_comp
    Longer than the epsilon proof, and that is worth being honest about. What it gains is that every step is a general fact about continuous maps rather than an estimate specific to addition.

    Differentiable implies continuous

    The last result in the file. Mathlib carries both facts, so the proof is to name them in order.

    If f is differentiable at x zero then f is continuous at x zero.
    theorem continuousAt_of_hasDerivAt
        (f : ℝ → ℝ) (x f' : ℝ)
        (hf : HasDerivAt f f' x) :
        ContinuousAt f x := by
      have hdiff : DifferentiableAt ℝ f x := hf.differentiableAt
      rcases hdiff with ⟨L, hL⟩
      exact hL.continuousAt
    A derivative is a linear map plus an error term. Destructure it, and the linear map's own continuity is the answer.
    06 What this is

    Plainly

    A reading project, and what it was for

    This was a semester in Berkeley's Directed Reading Program, working with a graduate mentor. It is not new mathematics and does not claim to be.

    The results are standard

    Filters date to Cartan in 1937. Every theorem here is in Mathlib already, often as the very lemma the proof calls. The exercise was stating them and getting them past the checker.

    The source is one file

    106 lines: two definitions, two lemmas, four theorems, one worked example. Nothing is stubbed and nothing is left open.

    Some proofs got longer

    The sum of two continuous functions is shorter with epsilons. The filter version wins on composition, on generality, and on not having to write the same argument once per limit notion.

    The framing is borrowed

    The count of 169 and the composition figure come from a Lean workshop talk. The convergence material follows a 2012 paper by Max Garcia; this project's contribution was formalizing his definitions and proofs.

    Nothing about ultrafilters

    Garcia's paper goes on to ultrafilters, compactness and the Bolzano–Weierstrass theorem. None of that is here. It is the obvious next thing.

    One dimension only

    Everything is stated for functions on the real line. Filters are what Mathlib uses in every dimension, so the extension is available, but it was not taken.

    Source

    github.com/Dronmong/Real-Analysis-with-Filters — the Lean file and the original write-up.

    Real Analysis with Filters — the paper this page is based on. Dron Mongia, mentor Thomas Browning, Fall 2025.

    References

    M. Garcia, Filters and Ultrafilters in Real Analysis. California Polytechnic State University, 2012. arXiv:1212.5740

    The mathlib Community, Mathematics in Lean. leanprover-community.github.io

    Lean Prover Community, Lean for the Curious Mathematician 2020. leanprover-community.github.io/lftcm2020 — the source of the 169 and 2,197 figures.

    Acknowledgement

    The original paper used AI assistance for LaTeX notation, page formatting, and inserting code snippets. This page was written with the assistance of Claude, an AI system made by Anthropic. The Lean quoted here is verbatim from the repository.

    Continue exploring

    Read the file, the original paper, or the formalization that came after it.