The knapsack problem is a classic combinatorial optimisation problem: given a collection of items each with a weight and a value, choose a subset of items whose total weight fits within a given capacity and whose total value is as large as possible. It is an excellent example for formal specification because the what (a maximum-value feasible selection) is easy to state precisely, even though the how (finding such a selection efficiently) is computationally hard. This example shows how Lean 4’s subtype mechanism allows us to characterise optimal solutions directly in the type system, with no thought at all for how an implementation might work.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/paulch42/lean-spec/llms.txt
Use this file to discover all available pages before exploring further.
Item
The first step is to define a structure representing a single item that can be placed in the knapsack.value (how desirable it is) and a cost (how much weight it contributes). The deriving BEq clause automatically generates a decidable equality instance, which is needed later for counting occurrences in lists. Note that two distinct items can have the same value and cost — the problem allows multiple copies of physically identical items.
A structure in Lean 4 is the equivalent of a record or struct in other languages, packaging separate fields into a single unit. As we will see in later definitions, Lean structures are considerably more expressive than records in most languages because fields can be propositions that constrain the values of other fields.
Cost and Value of a Selection
The total cost of a list of items is the sum of the individual item costs:Item.cost projection over the list to obtain a list of natural numbers, then sums them. Similarly, the total value is:
value, to be maximised) and the feasibility constraint (cost, which must not exceed the knapsack capacity).
SubList — Accounting for Duplicates
A candidate selection must be drawn from the available items. The right notion here is not a subset (which ignores duplicates) but a sublist that respects multiplicity: if a candidate selection contains two copies of some item, the source collection must also contain at least two copies of that item.SubList as bs holds when, for every element a, the number of times a appears in as does not exceed the number of times it appears in bs. This is a count-aware generalisation of the subset relation, and it correctly handles items with identical fields.
Candidate Solutions
Approach 1: A Predicate on Lists
A list of items is a candidate solution if it is a sublist of the available items and its total cost is within the knapsack’s capacity:Candidate₁ is a predicate: given a capacity, a source list, and a candidate list, it is the proposition that candidate is a feasible selection.
Approach 2: A Type of Candidates
Alternatively, we can package the feasibility conditions directly into a subtype:Candidate₂ capacity source is a feasible selection: the subtype condition guarantees that any value of this type already satisfies both the sublist and cost constraints. Nothing additional needs to be checked.
Specifying the Knapsack Problem
Knapsack₁ — Using the Predicate Approach
WithCandidate₁ as a predicate, the knapsack problem is specified as finding an item list that is a candidate solution and that no other candidate solution exceeds in value:
optimal must itself be a candidate (Candidate₁ capacity source optimal), and for every other list is that is also a candidate, the value of is must not exceed the value of optimal. This is a precise, executable-free characterisation of optimality.
This is a compelling demonstration of where formal specification excels. The specification captures the problem by characterising the properties any solution must exhibit, with no thought for how an implementation might search for or construct such a solution. The pattern — define feasibility, then assert that the result is at least as good as every other feasible solution — recurs throughout the specification of optimisation problems.
Knapsack₂ — Using the Type Approach
WhenCandidate₂ is used to define the type of feasible solutions, the knapsack specification becomes:
List Item but a Candidate₂ capacity source — an element that already carries proof of feasibility. As a result, the subtype property no longer needs to state that optimal is a candidate; it can simply assert that every other candidate has value no greater than optimal. The optimality condition is simpler and the feasibility condition is elevated into the type itself.
The .val projections (cand.val, optimal.val) access the underlying List Item from the Candidate₂ subtype, which is needed to apply the value function.
Comparing the Two Formulations
BothKnapsack₁ and Knapsack₂ correctly specify the same problem. The difference is where feasibility lives:
Knapsack₁ | Knapsack₂ | |
|---|---|---|
| Data component | List Item (unconstrained) | Candidate₂ (already feasible) |
| Subtype property | Must assert feasibility and optimality | Need only assert optimality |
| Quantifier range | All List Item that happen to be candidates | All Candidate₂ values (all feasible by construction) |
Knapsack₂ is generally preferable: by encoding feasibility in the type of the optimal value, the subtype property is kept simpler and any function returning a Candidate₂ is guaranteed feasible by type-checking alone.