-
Notifications
You must be signed in to change notification settings - Fork 40
feat: query complexity via free monads #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7c47d30
c0c3426
a5389ab
a32b00d
927e4e8
c8d3206
9c3c26c
2136afe
f5397b0
9e6fcd4
9381b9a
aeed9a7
b8c9a1e
9f1ba8a
0cd107c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| /- | ||
| Copyright (c) 2025 Tanner Duve. All rights reserved. | ||
| Released under Apache 2.0 license as described in the file LICENSE. | ||
| Authors: Tanner Duve | ||
| -/ | ||
|
|
||
| import Cslib.Algorithms.QueryModel | ||
|
|
||
| /-! | ||
| # Merge sort in the query model | ||
|
|
||
| This file implements merge sort as a program in the query model defined in | ||
| `Cslib.Algorithms.QueryModel`. The algorithm uses only comparison queries. | ||
|
|
||
| ## Main definitions | ||
|
|
||
| - `merge` : merge step using `Prog` comparisons | ||
| - `split` : split a list in two by alternating elements | ||
| - `mergeSort` : merge sort expressed in the query model | ||
|
|
||
| We also provide simple example evaluations of `mergeSort` and its time cost. | ||
| -/ | ||
|
|
||
| open Cslib | ||
|
|
||
| namespace Cslib.Algorithms.MergeSort.QueryBased | ||
|
|
||
| open Cslib.Algorithms | ||
|
|
||
|
|
||
| /-- The Model for comparison sorting natural-number registers.-/ | ||
| inductive QueryF : Type → Type where | ||
| /-- Read the value stored at index `i`. -/ | ||
| | read : Nat → QueryF Nat | ||
| /-- Write value `v` at index `i`. -/ | ||
| | write : Nat → Nat → QueryF PUnit | ||
|
Comment on lines
+35
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you refer to names in the docstring they should exist in the code /-- Write value `v` at index `i`. -/
| write (i : Nat) (v : Nat) : QueryF PUnitetc
Comment on lines
+33
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fact your sorting implementation doesn't use these at all suggests that they're not really useful as query operators. Even if you did force the API to manually
which makes counting reads and writes pretty uninteresting.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The correct model would have |
||
| /-- Compare the values at indices `i` and `j`. -/ | ||
| | cmp : Nat → Nat → QueryF Bool | ||
|
|
||
| /-- Lift a comparison into the query language at the top level. -/ | ||
| def cmpVal (x y : Nat) : Prog Bool := | ||
| FreeM.lift (QueryF.cmp x y) | ||
|
|
||
| /-- Concrete semantics for primitive queries, used to run programs. -/ | ||
| def evalQuery : {ι : Type} → QueryF ι → ι | ||
| | _, .read _ => 0 | ||
| | _, .write _ _ => PUnit.unit | ||
| | _, .cmp x y => x ≤ y | ||
|
Comment on lines
+45
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be more like def evalQuery [LE α] [Inhabited α] : {ι : Type} → QueryF ι → StateT (Array α) ι
| _, .read i => return (← get)[i]!
| _, .write i x => do modify fun a => a.set! i x
| _, .cmp i j => do let a ← get; return a[i]! ≤ a[j]!You need a real model if you want to prove correctness There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks. That's a valid point. Fwiw, this entire example has several points where it is still possible to sneak in flawed code. Currently I am trying to get the typeclasses and composition theorems right, then I can redo this example.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do note that |
||
|
|
||
|
|
||
|
|
||
|
|
||
| abbrev SortProg := Prog QueryF | ||
|
|
||
| /-- Lift a comparison on values into the free monad. -/ | ||
| def cmpVal (x y : Nat) : SortProg Bool := | ||
| FreeM.lift (QueryF.cmp x y) | ||
|
|
||
| /-- Merge two sorted lists using comparisons in the query monad. -/ | ||
| def merge : List Nat → List Nat → SortProg (List Nat) | ||
| | [], ys => pure ys | ||
| | xs, [] => pure xs | ||
| | x :: xs', y :: ys' => do | ||
| let b ← cmpVal x y | ||
| if b then | ||
| let rest ← merge xs' (y :: ys') | ||
| pure (x :: rest) | ||
| else | ||
| let rest ← merge (x :: xs') ys' | ||
| pure (y :: rest) | ||
|
|
||
| /-- Split a list into two lists by alternating elements. -/ | ||
| def split (xs : List Nat) : List Nat × List Nat := | ||
| let rec go : List Nat → List Nat → List Nat → List Nat × List Nat | ||
| | [], accL, accR => (accL.reverse, accR.reverse) | ||
| | [x], accL, accR => ((x :: accL).reverse, accR.reverse) | ||
| | x :: y :: xs, accL, accR => go xs (x :: accL) (y :: accR) | ||
| go xs [] [] | ||
|
|
||
| /-- Merge sort expressed as a program in the query model. | ||
| TODO: Working version without partial -/ | ||
| partial def mergeSort : List Nat → SortProg (List Nat) | ||
| | [] => pure [] | ||
| | [x] => pure [x] | ||
| | xs => | ||
| let (left, right) := split xs | ||
| do | ||
| let sortedLeft ← mergeSort left | ||
| let sortedRight ← mergeSort right | ||
| merge sortedLeft sortedRight | ||
|
|
||
| #eval evalProg (mergeSort [5,3,8,6,2,7,4,1]) | ||
| #eval timeProg (mergeSort [5,3,8,6,2,7,4,1]) | ||
|
|
||
| end Cslib.Algorithms.MergeSort.QueryBased | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| /- | ||
| Copyright (c) 2025 Tanner Duve. All rights reserved. | ||
| Released under Apache 2.0 license as described in the file LICENSE. | ||
| Authors: Tanner Duve | ||
| -/ | ||
|
|
||
| import Cslib.Foundations.Control.Monad.Free.Effects | ||
| import Cslib.Foundations.Control.Monad.Free.Fold | ||
| import Cslib.Foundations.Control.Monad.Time | ||
|
|
||
| /- | ||
| # Query model | ||
|
|
||
| This file defines a simple query language modeled as a free monad over a | ||
| parametric type of query operations. | ||
|
|
||
| We equip this language with a cost model (`TimeM`) that counts how many primitive queries | ||
| are performed. An example algorithm (merge sort) is implemented in | ||
| `Cslib.Algorithms.MergeSort.QueryBased`. | ||
|
|
||
| ## Main definitions | ||
|
|
||
| - `QueryF`, `Prog` : query language and programs | ||
| - `timeOfQuery`, `timeInterp`, `timeProg` : cost model for programs | ||
| - `evalQuery`, `evalProg` : concrete execution semantics | ||
|
|
||
| ## Tags | ||
|
|
||
| query model, free monad, time complexity, merge sort | ||
| -/ | ||
|
|
||
| namespace Cslib | ||
|
|
||
| namespace Algorithms | ||
|
|
||
|
|
||
|
|
||
| /-- Programs built as the free monad over `QueryF`. -/ | ||
| abbrev Prog (QType : Type u → Type u) (α : Type v) := FreeM QType α | ||
|
|
||
|
|
||
| instance {QType : Type u → Type u} : Monad (Prog QType) := inferInstance | ||
|
|
||
|
|
||
|
|
||
|
|
||
| namespace Prog | ||
|
|
||
|
|
||
|
|
||
| /-- Conditional branching on a boolean program. -/ | ||
| def cond {QType} {α} (b : Prog QType Bool) (t e : Prog QType α) : Prog QType α := | ||
| b.bind (fun b' => if b' then t else e) | ||
|
|
||
| /-- A counting loop from `0` to `n - 1`, sequencing the body. -/ | ||
| def forLoop {QType} (n : Nat) (body : Nat → Prog QType PUnit) : Prog QType PUnit := | ||
| go n | ||
| where | ||
| /-- Auxiliary recursive worker for `forLoop`. -/ | ||
| go : Nat → Prog QType PUnit | ||
| | 0 => pure () | ||
| | i + 1 => | ||
| body i >>= fun _ => go i | ||
|
|
||
| end Prog | ||
|
Comment on lines
+47
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed on zulip, this is |
||
|
|
||
| class Query (Q : Type u → Type u) where | ||
| timeOfQuery : {ι : Type u} → Q ι → Nat | ||
| evalQuery : {ι : Type u} → Q ι → ι | ||
|
|
||
| open Query | ||
| -- /-- Constant time cost assigned to each primitive query. -/ | ||
| -- def timeOfQuery : {ι : Type} → QueryF ι → Nat | ||
| -- | _, .read _ => 1 | ||
| -- | _, .write _ _ => 1 | ||
| -- | _, .cmp _ _ => 1 | ||
|
|
||
| /-- | ||
| Interpret primitive queries into the time-counting monad `TimeM`. | ||
| -/ | ||
| def timeInterp [Query QF] [Inhabited ι] (q : QF ι) : TimeM ι := | ||
| TimeM.tick default (timeOfQuery q) | ||
|
|
||
| -- /-- Interpret primitive queries into the time-counting monad `TimeM`. -/ | ||
| -- def timeInterp : {ι : Type} → QueryF ι → TimeM ι | ||
| -- | _, .read i => TimeM.tick 0 (timeOfQuery (.read i)) | ||
| -- | _, .write i v => TimeM.tick PUnit.unit (timeOfQuery (.write i v)) | ||
| -- | _, .cmp i j => TimeM.tick false (timeOfQuery (.cmp i j)) | ||
|
|
||
|
|
||
| instance {α : Type u} {Q : Type u → Type u} [Query Q] : MonadLiftT (Prog Q) TimeM where | ||
| monadLift (p : Prog Q α) : TimeM α := | ||
| timeInterp | ||
|
|
||
| /-- Total time cost of running a program under the interpreter `timeInterp`. -/ | ||
| def timeProg [Query QF] {α : Type u} (p : Prog QF α) : Nat := | ||
| (p.liftM timeInterp).time | ||
|
|
||
|
|
||
|
|
||
| /-- Evaluate a query program to a pure value using `evalQuery`. -/ | ||
| def evalProg [Query QF] {α : Type} (p : Prog QF α) : α := | ||
| FreeM.foldFreeM id | ||
| (fun {ι} (op : QF ι) (k : ι → α) => | ||
| k (evalQuery op)) | ||
| p | ||
|
|
||
| end Algorithms | ||
|
|
||
| end Cslib | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| /- | ||
| Copyright (c) 2025 Sorrachai Yingchareonthawornhcai. All rights reserved. | ||
| Released under Apache 2.0 license as described in the file LICENSE. | ||
| Authors: Sorrachai Yingchareonthawornhcai, Tanner Duve | ||
| -/ | ||
|
|
||
| import Mathlib.Control.Monad.Writer | ||
|
|
||
| /-! | ||
| # Time Monad | ||
|
|
||
| `TimeM` is a monad that tracks execution time alongside computations, using natural numbers | ||
| as a simple cost model. As plain types it is isomorphic to `WriterT Nat Id`. | ||
| -/ | ||
|
|
||
| structure TimeM (α : Type u) where | ||
| /-- The result of the computation. -/ | ||
| ret : α | ||
| /-- The accumulated time cost. -/ | ||
| time : Nat | ||
|
|
||
| namespace TimeM | ||
|
|
||
| def pure {α} (a : α) : TimeM α := | ||
| ⟨a, 0⟩ | ||
|
|
||
| def bind {α β} (m : TimeM α) (f : α → TimeM β) : TimeM β := | ||
| let r := f m.ret | ||
| ⟨r.ret, m.time + r.time⟩ | ||
|
|
||
| instance : Monad TimeM where | ||
| pure := pure | ||
| bind := bind | ||
|
|
||
| @[simp] def tick {α : Type} (a : α) (c : ℕ := 1) : TimeM α := | ||
| ⟨a, c⟩ | ||
|
|
||
| scoped notation "✓" a:arg ", " c:arg => tick a c | ||
| scoped notation "✓" a:arg => tick a -- Default case with only one argument | ||
|
|
||
| def tickUnit : TimeM Unit := | ||
| ✓ () -- This uses the default time increment of 1 | ||
|
|
||
| @[simp] theorem time_of_pure {α} (a : α) : (pure a).time = 0 := rfl | ||
| @[simp] theorem time_of_bind {α β} (m : TimeM α) (f : α → TimeM β) : | ||
| (TimeM.bind m f).time = m.time + (f m.ret).time := rfl | ||
| @[simp] theorem time_of_tick {α} (a : α) (c : ℕ) : (tick a c).time = c := rfl | ||
| @[simp] theorem ret_bind {α β} (m : TimeM α) (f : α → TimeM β) : | ||
| (TimeM.bind m f).ret = (f m.ret).ret := rfl | ||
|
|
||
| -- this allow us to simplify the chain of compositions | ||
| attribute [simp] Bind.bind Pure.pure TimeM.pure | ||
|
|
||
| /-- `TimeM` is (definitionally) the same as the writer monad `WriterT Nat Id`. -/ | ||
| abbrev WriterNat (α : Type) := WriterT Nat Id α | ||
|
|
||
| /-- Equivalence between `TimeM α` and `WriterT Nat Id α` as plain types. -/ | ||
| def equivWriter (α : Type) : TimeM α ≃ WriterNat α where | ||
| toFun m := (m.ret, m.time) | ||
| invFun w := ⟨w.1, w.2⟩ | ||
| left_inv m := by cases m; rfl | ||
| right_inv w := by cases w; rfl | ||
|
|
||
| @[simp] lemma equivWriter_toFun {α : Type} (m : TimeM α) : | ||
| (equivWriter α m : WriterNat α) = (m.ret, m.time) := rfl | ||
|
|
||
| @[simp] lemma equivWriter_invFun {α : Type} (w : WriterNat α) : | ||
| (equivWriter α).invFun w = TimeM.mk w.1 w.2 := rfl | ||
|
|
||
| end TimeM |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not generalize to arbitrary registers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still WIP. I will make this change once I have generalized the query type, and I am instantiating it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR started from a proof-of-concept in the zulip, posted by Tanner. It has some way to go.