feat(Query): query complexity framework with sorting examples - #401
feat(Query): query complexity framework with sorting examples#401kim-em wants to merge 85 commits into
Conversation
Co-authored-by: Eric Wieser <wieser.eric@gmail.com>
…o query-final-squash
Co-authored-by: Shreyas Srinivas <Shreyas4991@users.noreply.github.com> Co-authored-by: Eric Wieser <eric-wieser@users.noreply.github.com> Co-authored-by: Tanner Duve <tannerduve@gmail.com>
|
Another mistake in the descr :
The query combinator approach |
Not quite true. You can state correctness uniformly over a family of correct models. This is possible if this family of models is not a singleton and if different models may produce different outputs, as happens with sorting. It fails when the correct model is uniquely defined (as in determinants). That being said, proving uniform correctness over a family of models is a bit overkill. |
Replace the alternating odds/evens split with the contiguous split used by List.mergeSort, so that evaluating the query program against any oracle produces literally the same list as List.mergeSort with the comparator induced by the oracle. The new eval_mergeSort identification (mirroring eval_insertionSort) lets the permutation and sortedness proofs transfer directly from the List.mergeSort API instead of being restated by hand, and makes the query-based sort stable. The n * clog 2 n query bound is unchanged: the contiguous halves have the same lengths as the alternating ones, so the counting recurrence and arithmetic are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
…nto combined-query-complexity
eric-wieser
left a comment
There was a problem hiding this comment.
I'm reasonably happy with this now, though let's wait for discussion to stop in the CSLib reviewer channel before finally merging.
Generalize Bounds to query families Q : Type u → Type v, add a combinator deriving UpperBound from a pointwise count bound and monotonicity, and a sandwich lemma showing a LowerBound never exceeds an UpperBound for the same program. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
The triangular bound is attained by the all-false oracle; the previous n ^ 2 bound remains as a corollary and the UpperBound instance now goes through UpperBound.of_pointwise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
State countQueries_mergeSort_cons_cons with List.mergeSort arguments (the form eval_mergeSort rewrites to), isolate the List.mergeSort.eq_3 use in a private helper linking leanprover/lean4#14995, and derive mergeSort_upperBound through UpperBound.of_pointwise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Under an oracle implementing an antisymmetric total transitive relation, all correct comparison sorts produce the same output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Function.Injective.extend_sum_inl_inr is proposed in leanprover-community/mathlib4#43325 (with a golfed LeftInverse proof, mirrored here) and the Std.Total (InvImage r f) instance in leanprover-community/mathlib4#43326; keeping the local copies private avoids conflicts when those land. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Instantiate the comparison-sorting lower bound at mergeSort and compose it with the upper bound, yielding clog 2 n! <= n * clog 2 n for free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Replace the private cons-cons unfolding with a mirror of the mergeSort_append lemma proposed in leanprover/lean4#14995 (merging the sorted halves of any balanced split gives mergeSort), deriving the split form from it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
@eric-wieser I was told there would be a discussion comparing #685 and #401 involving me. Why is it that this PR is being merged directly. It is still suboptimal in design. I have waited This was discussed in the cslib meetings. For the record I maintain that #685 should be merged. This PR (401) has done a good job of performing what amounts to a shallow copy of my work. However #372 (and #685) makes better design choices and has been battle tested. It has a better downstream track record. It is also extremely dishonest to claim that subsequent reviews have improved it over #372. At best it is an inadequate approximation, with some minor changes that can be PRed back to #685 (successor of #372 which contains this PR's history, and was made at the maintainers' request). |
…4995) This PR adds two lemmas exposing the recursion of `List.mergeSort` without reference to `MergeSort.Internal.splitInTwo`: - `mergeSort_append`: merging the sorted halves of any balanced split (`l₂.length ≤ l₁.length ≤ l₂.length + 1`) gives `(l₁ ++ l₂).mergeSort`. This is the primary statement: it has no index arithmetic, holds uniformly for every list length, and any specific unfolding (take/drop at the midpoint, cons-cons forms) is a two-line corollary. - `@[simp] mergeSort_pair`: `[a, b].mergeSort le = if le a b then [a, b] else [b, a]`, completing the `mergeSort_nil`/`mergeSort_singleton` progression. Unlike `mergeSort_append` it genuinely simplifies, so it is marked `@[simp]`. Downstream libraries currently have to use the auto-generated `List.mergeSort.eq_3` (whose numbering is unstable, and which is not accessible from files using the module system without `import all`) or unfold `splitInTwo`'s subtype plumbing by hand; this came up in leanprover/cslib#401, where a query-complexity model of merge sort is proved to agree with `List.mergeSort`. 🤖 Prepared with Claude Code Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Replace the separate finiteness and Nat.card hypotheses of FreeM.exists_countQueries_ge_clog with one Cardinal inequality (a natural bound on a cardinal implies finiteness), per Eric's review suggestion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
|
Additionally I wish to note that in any sensible open source project that respects its contributors, a PR with this much overlap with a prior PR would be closed as a duplicate PR. The contributor would be asked to build on top of existing work. Senior members in particular wouldn’t scoop the work of junior members (with/without AI) Even Google's AI knows this. I am sure a Claude user can figure this out : |
| /-- Sort a list using insertion sort with comparison queries. -/ | ||
| @[expose] def insertionSort : List α → FreeM (LEQuery α) (List α) | ||
| /-- Sort a list using insertion sort with monadic comparisons. -/ | ||
| @[expose] def insertionSortM : List α → m (List α) |
There was a problem hiding this comment.
What monad are you planning to use other than free monads?
There was a problem hiding this comment.
This change you are applying you could be used to rewrite all monadic functions in all of lean and mathlib, including tactic monads and tactics in a monad polymorphic way. That doesn't mean one should. This is the design used by so-called mtl style transformers. It doesn't add anything meaningful here.
There was a problem hiding this comment.
Another point : This doesn't scale for large and composite query models. It results in redundancy in parameters Explained here on zulip
There was a problem hiding this comment.
What monad are you planning to use other than free monads?
Id and PFunctor.FreeM are two natural choices; but you could equally do something silly like IO for a game where you ask the human to do the comparison, or perhaps some kind of LogM monad that records the comparisons as they happen.
Of course you can get here by starting with something in FreeM and using liftM, but my guess is that Lean's compiler cannot optimize this to anywhere near the same extent.
The actual motivation for this is to:
- present a pattern that allow algorithms to be written without CSLib, but then have their complexity proved downstream in CSLib
- allow monad-generic implementations to be proved lawful, in the sense that they are preserved under
IsMonadHom(feat: add a predicate for monad morphisms #856).
There was a problem hiding this comment.
This change you are applying you could be used to rewrite all monadic functions in all of lean and mathlib,
See List.find/List.findM, List.any/List.anyM, etc; there is lots of precedent for already doing this.
including tactic monads and tactics in a monad polymorphic way
To some extend the functions written with [MonadEnv m] instead of CoreM are also opting into this pattern.
There was a problem hiding this comment.
Transitioning to PFunctor.FreeM is much simpler since PFunctor.FreeM generalizes FreeM.
I'd encourage you to start a Zulip thread comparing these. As I understand it, there are queries in FreeM that have no representation in PFunctor.FreeM and vice versa. I think this is not well-explained by the current docstring of PFunctor.FreeM, and it would be great to construct contrived or even plausible examples of each.
There was a problem hiding this comment.
It's not true though.
- FreeM and this MTL style approach are actually complementary. They are both separately used to compose multiple effects to achieve effectful programming.
- Secondly to construct the interface, you would compose an existing queue type, an existing stack type, and an existing fibonacci heap type using direct sums, and use the composition lemmas for these. Writing a bespoke structure means we can't directly use those lemmas.
There was a problem hiding this comment.
Transitioning to PFunctor.FreeM is much simpler since PFunctor.FreeM generalizes FreeM.
I'd encourage you to start a Zulip thread comparing these. As I understand it, there are queries in FreeM that have no representation in PFunctor.FreeM and vice versa. I think this is not well-explained by the current docstring of
PFunctor.FreeM, and it would be great to construct contrived or even plausible examples of each.
Michael Sammler already explained how PFunctor.FreeM can express everything FreeM can but not vice versa. Quang Dao corrected it:
There was a problem hiding this comment.
There was a problem hiding this comment.
I wish to note that Eric and I discussed on Zulip that this change is orthogonal to the query combinator model and has been refactored to #861. Further we discussed that this change is of no use to algorithmic theory.
Given the amount of misunderstanding expressed by several people about this framework, this content only serves to obfuscate the above points to maintainers.
The generic List.orderedInsertM/insertionSortM commute with any monad morphism, stated with the IsMonadHom laws of leanprover#856 inlined and needing no lawfulness on either side. Since evaluation against an oracle is a monad morphism to Id, the executable Id instantiation is List.insertionSort with no separate proof about the generic definition, and the framework's complexity bounds apply to the generic program definitionally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pxy48TaP92UgEq28KGm8BG
This PR adds a
FreeM-based framework for proving upper and lower bounds on query complexity. It incorporates and preserves the history of the earlier work in #372, together with subsequent design and review contributions discussed in the CSLib Algorithm frameworks thread.A program is represented directly as
FreeM F α, whereF : Type u → Type vmaps each query to its response type. The program is constructed independently of an oracle; evaluation supplies oracle responses only through lifted queries, while allowing later queries to depend on earlier responses.The framework provides three universe-polymorphic interpreters, defined through
FreeM.liftM:FreeM.evalevaluates a query program against an oracle.FreeM.countQueriescounts the queries made along the oracle-determined execution path.FreeM.costassigns query-dependent weights in an arbitrary additive monoid.Pure computation is uncharged: costs attach only to lifted queries, so the interpreters measure query complexity. Counting is structural, derived from the program tree, rather than relying on trusted annotations as in a
TimeM-style analysis. TheFreeMmodule docstring includes a short recipe for setting up a new query type.A caveat is that only operations represented by
FreeM.liftare counted. If the intended oracle can be reconstructed from structure otherwise available to the program—for example, if its answers are determined by laws and computable using ordinary Lean definitions—an implementation may reimplement the oracle internally and avoid counted calls. The model is therefore strongest when oracle operations remain abstract. Parametric problems are less vulnerable: for example, an algorithm uniform over an arbitrary ring, with ring operations exposed only throughArithQuery, cannot generally reproduce those operations without issuing the corresponding queries. Parametricity alone is not sufficient if the same operations are also available directly through typeclass instances or other definitions.Bounds and general lower-bound theorem
UpperBoundandLowerBoundexpress query bounds quantified over oracles.UpperBound.of_pointwisederives an upper bound from a per-input count bound and monotonicity of the bound function, andLowerBound.le_upperBoundshows that a lower bound for a program never exceeds an upper bound for it.The central combinatorial result is
FreeM.exists_countQueries_ge_clog: ifr, andnoracles produce distinct results from a fixed program,then some oracle forces the program to make at least
⌈log_r n⌉queries.The proof works directly on
FreeM, without a separate fixed-responseQueryTreedatatype.Sorting
The PR defines comparison queries
LEQuery, a correctness specificationIsSort, and query implementations of insertion sort and merge sort mirroringList.insertionSortandList.mergeSortexactly:eval_insertionSortandeval_mergeSortidentify each program, evaluated against any oracle, with the corresponding standard-library function, so correctness (permutation, sortedness, and, for merge sort, stability) transfers from the existing API rather than being reproved.IsSort.eval_eqshows the specification pins down the behaviour: under any oracle implementing an antisymmetric total transitive relation, all correct comparison sorts produce the same output.Proved bounds:
n * (n - 1) / 2queries, a bound attained by the all-falseoracle, withn²as a corollary;n * ⌈log₂ n⌉queries;⌈log₂(n!)⌉, by constructingn!hidden total orders with distinct sorted outputs and applying the generalFreeMlower-bound theorem;LowerBound.le_upperBoundyields the arithmetic fact⌈log₂(n!)⌉ ≤ n * ⌈log₂ n⌉with no further work.Weighted-cost example
The arithmetic example demonstrates query-dependent costs using naive complex multiplication and Gauss's trick:
4 * c_mul + 2 * c_add;3 * c_mul + 5 * c_add;3 * c_add ≤ c_mul.Upstream mirrors
Three general-purpose declarations are proposed upstream and kept
privatehere until they land:List.mergeSort_append(leanprover/lean4#14995),Function.Injective.extend_sum_inl_inr(leanprover-community/mathlib4#43325), and theStd.Total (InvImage r f)instance (leanprover-community/mathlib4#43326).Files
Query/FreeM.leanQuery/Bounds.leanUpperBound,LowerBound,of_pointwise,le_upperBoundQuery/Arith/{Defs,Lemmas}.leanQuery/Sort/LEQuery.leanQuery/Sort/IsSort.leanQuery/Sort/Insertion/{Defs,Lemmas}.leanList.insertionSort, triangular boundQuery/Sort/Merge/{Defs,Lemmas}.leanList.mergeSort,n * ⌈log₂ n⌉boundQuery/Sort/Merge/Bounds.lean⌈log₂(n!)⌉ ≤ n * ⌈log₂ n⌉corollaryQuery/Sort/LowerBound.lean⌈log₂(n!)⌉comparison-sorting lower boundCslibTests/Query.lean