Skip to content

Commit d6c0b90

Browse files
feat(Data/PFunctor): add free monad of a polynomial functor (#477)
## Summary - Port `PFunctor.FreeM` from [VCV-io](https://github.com/dtumad/VCV-io) (`ToMathlib/PFunctor/Free.lean`) to cslib. - The free monad on a polynomial functor extends the W-type construction with an extra `pure` constructor, yielding a lawful monad that is free over `P : PFunctor`. ### Main definitions - `PFunctor.FreeM`: inductive type with `pure` and `roll` constructors - `FreeM.lift` / `FreeM.liftA`: lifting from the base polynomial functor - `Monad` and `LawfulMonad` instances - `FreeM.inductionOn` / `FreeM.construct`: propositional and dependent eliminators - `FreeM.mapM`: canonical interpretation into any target monad, with `simp` lemmas for `bind`, `map`, `seq`, etc. ### Notes - The `MonadHom`-related definitions (`mapMHom`, `mapMHom'`) from the original VCV-io source are omitted since cslib does not have `MonadHom` infrastructure. These can be added later if cslib gains monad homomorphism support. - File placed at `Cslib/Foundations/Data/PFunctor/FreeM.lean` as a foundation for future polynomial functor work. - Builds cleanly with no linter warnings. Posted by Cursor assistant (model: claude-4.6-opus-high-thinking) on behalf of the user (Quang Dao) with approval. Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1f601a2 commit d6c0b90

3 files changed

Lines changed: 380 additions & 0 deletions

File tree

Cslib.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public import Cslib.Foundations.Data.OmegaSequence.Flatten
6464
public import Cslib.Foundations.Data.OmegaSequence.InfOcc
6565
public import Cslib.Foundations.Data.OmegaSequence.Init
6666
public import Cslib.Foundations.Data.OmegaSequence.Temporal
67+
public import Cslib.Foundations.Data.PFunctor.Free
6768
public import Cslib.Foundations.Data.RelatesInSteps
6869
public import Cslib.Foundations.Data.Relation
6970
public import Cslib.Foundations.Data.Set.Saturation

Cslib/Foundations/Control/Monad/Free.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ This unique interpreter is `FreeM.liftM f`
3939
- `FreeM.liftM_unique`: Proof of the universal property
4040
4141
For elimination and interpretation theory, see `Free/Fold.lean`.
42+
For polynomial effect signatures with explicit operation shapes and positions, see
43+
`Cslib.Foundations.Data.PFunctor.Free`.
4244
4345
See the Haskell [freer-simple](https://hackage.haskell.org/package/freer-simple) library for the
4446
Haskell implementation that inspired this approach.
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
/-
2+
Copyright (c) 2026 Quang Dao. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Quang Dao
5+
-/
6+
7+
module
8+
9+
public import Cslib.Init
10+
public import Mathlib.Data.PFunctor.Univariate.Basic
11+
12+
/-!
13+
# Free Monad of a Polynomial Functor
14+
15+
We define the free monad on a **polynomial functor** (`PFunctor`), and prove some basic properties.
16+
17+
The free monad `PFunctor.FreeM P` extends the W-type construction with an extra `pure`
18+
constructor, yielding a monad that is free over the polynomial functor `P`.
19+
20+
## Comparison with `Cslib.FreeM`
21+
22+
`Cslib.FreeM F` (in `Cslib/Foundations/Control/Monad/Free.lean`) builds a free monad over an
23+
arbitrary type constructor `F : Type u → Type v`, which need not be functorial.
24+
Its `liftBind` constructor abstracts over the intermediate type `ι`:
25+
```
26+
| liftBind {ι : Type u} (op : F ι) (cont : ι → FreeM F α) : FreeM F α
27+
```
28+
29+
`PFunctor.FreeM P` instead takes a polynomial functor `P : PFunctor`, where the shapes
30+
`P.A` and positions `P.B a` are given explicitly.
31+
Its `liftBind` constructor uses the shape and continuation directly:
32+
```
33+
| liftBind (a : P.A) (cont : P.B a → P.FreeM α) : P.FreeM α
34+
```
35+
36+
When the effect signature is naturally polynomial (a fixed set of operations, each with a
37+
known return type), `PFunctor.FreeM` avoids the universe bump that the abstract `ι` in
38+
`Cslib.FreeM` introduces.
39+
Concretely, `PFunctor.FreeM P` is a genuine endofunctor on a single universe: for a ground
40+
`P`, `P.FreeM α : Type` whenever `α : Type`, whereas `Cslib.FreeM F α : Type 1` for
41+
`F : Type → Type`, since `liftBind` stores the intermediate type `ι : Type`.
42+
43+
This matters when a program must itself be a first-class value of the same kind, i.e. for
44+
higher-order effects whose operations consume or return computations of the same monad
45+
(schedulers, exception handlers, staged interpreters, higher-order oracles).
46+
Such an effect's response type can be another `P.FreeM` computation, staying in one universe:
47+
```
48+
def coin : PFunctor.{0,0} := ⟨Bool, fun b => if b then Bool else Nat⟩
49+
-- a `coin`-program is itself `Type 0`, so it can be another effect's response type:
50+
def scheduler : PFunctor.{0,0} := ⟨Unit, fun _ => coin.FreeM Bool⟩ -- `Type 0`
51+
```
52+
With the abstract `ι`, the analogous program lives in `Type 1`, so an effect `Type → Type`
53+
cannot return it; bumping the effect to `Type 1 → Type 1` pushes its programs to `Type 2`,
54+
and so on without bound.
55+
56+
This construction is ported from the [VCV-io](https://github.com/dtumad/VCV-io) library.
57+
58+
## Main Definitions
59+
60+
- `PFunctor.FreeM`: The free monad on a polynomial functor.
61+
- `PFunctor.FreeM.lift`: Lift a shape of the base polynomial functor into the free monad.
62+
- `PFunctor.FreeM.liftObj`: Lift an object of the base polynomial functor into the free monad.
63+
- `PFunctor.FreeM.liftM`: Interpret `FreeM P` into any other monad.
64+
-/
65+
66+
@[expose] public section
67+
68+
universe u v uA uB
69+
70+
namespace PFunctor
71+
72+
-- Disable generation of unneeded lemmas which the simpNF linter would complain about.
73+
set_option genInjectivity false in
74+
set_option genSizeOfSpec false in
75+
/-- The free monad on a polynomial functor.
76+
This extends `WType` with an extra `pure` constructor. -/
77+
inductive FreeM (P : PFunctor.{uA, uB}) : Type v → Type (max uA uB v)
78+
/-- A leaf node wrapping a pure value. -/
79+
| protected pure {α} (a : α) : P.FreeM α
80+
/-- Invoke the operation `a : P.A` with continuation `cont : P.B a → P.FreeM α`. -/
81+
| liftBind {α} (a : P.A) (cont : P.B a → P.FreeM α) : P.FreeM α
82+
deriving Inhabited
83+
84+
namespace FreeM
85+
86+
variable {P : PFunctor.{uA, uB}} {α β γ : Type*}
87+
88+
instance : Pure (P.FreeM) where pure := .pure
89+
90+
@[simp]
91+
theorem pure_eq_pure : (FreeM.pure : α → P.FreeM α) = pure := rfl
92+
93+
/-- Lift a shape of the base polynomial functor into the free monad. -/
94+
def lift (a : P.A) : P.FreeM (P.B a) := FreeM.liftBind a pure
95+
96+
@[simp] lemma lift_ne_pure (a : P.A) (y : P.B a) :
97+
(lift a : P.FreeM (P.B a)) ≠ pure y := by simp [lift]
98+
99+
@[simp] lemma pure_ne_lift (a : P.A) (y : P.B a) :
100+
pure y ≠ (lift a : P.FreeM (P.B a)) := by simp [lift]
101+
102+
/-- Bind operation for the `FreeM` monad.
103+
104+
The builtin `>>=` notation should be preferred when `α` and `β` are in the same universe. -/
105+
protected def bind : P.FreeM α → (α → P.FreeM β) → P.FreeM β
106+
| FreeM.pure a, f => f a
107+
| FreeM.liftBind a cont, f => FreeM.liftBind a (fun u ↦ FreeM.bind (cont u) f)
108+
109+
instance : Bind (P.FreeM) where bind := .bind
110+
111+
/-- Note that this lemma does not always apply, as it is universe-constrained by `Bind.bind`. -/
112+
@[simp]
113+
theorem bind_eq_bind {α β : Type v} :
114+
(FreeM.bind : P.FreeM α → _ → P.FreeM β) = Bind.bind := rfl
115+
116+
/-- Map a function over a `FreeM` computation.
117+
118+
The builtin `<$>` notation should be preferred when `α` and `β` are in the same universe. -/
119+
def map (f : α → β) : P.FreeM α → P.FreeM β
120+
| .pure a => .pure (f a)
121+
| .liftBind a cont => .liftBind a fun u => FreeM.map f (cont u)
122+
123+
instance : Functor (P.FreeM) where
124+
map := .map
125+
126+
/-- Note that this lemma does not always apply, as it is universe-constrained by `Functor.map`. -/
127+
@[simp]
128+
theorem map_eq_map {α β : Type v} :
129+
FreeM.map (P := P) (α := α) (β := β) = Functor.map := rfl
130+
131+
@[simp]
132+
lemma liftBind_eq (a : P.A) (cont : P.B a → P.FreeM α) :
133+
FreeM.liftBind a cont = (FreeM.lift a).bind cont := rfl
134+
135+
/-- Lift an object of the base polynomial functor into the free monad.
136+
137+
This lifts the shape `x.1` with `lift` and relabels the responses with `x.2`. We use the
138+
universe-polymorphic `FreeM.map` rather than `<$>`, since the response type `P.B x.1` and the
139+
target `α` need not lie in the same universe. -/
140+
abbrev liftObj (x : P.Obj α) : P.FreeM α := (lift x.1).map x.2
141+
142+
instance : MonadLift P (P.FreeM) where
143+
monadLift x := FreeM.liftObj x
144+
145+
@[simp] lemma liftObj_ne_pure (x : P.Obj α) (y : α) :
146+
(liftObj x : P.FreeM α) ≠ pure y := by simp [liftObj, lift, map, -liftBind_eq]
147+
148+
@[simp] lemma pure_ne_liftObj (x : P.Obj α) (y : α) :
149+
pure y ≠ (liftObj x : P.FreeM α) := by simp [liftObj, lift, map, -liftBind_eq]
150+
151+
lemma monadLift_eq_liftObj (x : P.Obj α) : (x : P.FreeM α) = FreeM.liftObj x := rfl
152+
153+
set_option linter.unusedVariables false in
154+
/-- An override for the default induction principle that is in simp-normal form.
155+
156+
Note that when `α` and `P.B a` are in the same universe, this simplifies slightly further. -/
157+
@[induction_eliminator]
158+
protected theorem induction {motive : P.FreeM α → Prop}
159+
(pure : ∀ a, motive (pure a))
160+
(lift_bind : ∀ (a : P.A) (cont : P.B a → P.FreeM α) (ih : ∀ i, motive (cont i)),
161+
motive ((FreeM.lift a).bind cont)) : ∀ x, motive x
162+
| .pure a => pure a
163+
| liftBind a cont => lift_bind a cont fun u => FreeM.induction pure lift_bind (cont u)
164+
165+
protected theorem bind_assoc (x : P.FreeM α) (f : α → P.FreeM β) (g : β → P.FreeM γ) :
166+
(x.bind f).bind g = x.bind (fun a => (f a).bind g) := by
167+
induction x with
168+
| pure a => rfl
169+
| lift_bind a cont ih => simp [← liftBind_eq, FreeM.bind, ih] at *
170+
171+
/-- `.pure a` followed by `bind` collapses immediately. -/
172+
@[simp]
173+
lemma pure_bind (a : α) (f : α → P.FreeM β) :
174+
(pure a : P.FreeM α).bind f = f a := rfl
175+
176+
@[simp]
177+
lemma bind_pure : ∀ x : P.FreeM α, x.bind pure = x
178+
| .pure a => rfl
179+
| .liftBind a cont => by
180+
simp only [FreeM.bind]; congr 1; funext u; exact bind_pure (cont u)
181+
182+
@[simp]
183+
lemma bind_pure_comp (f : α → β) : ∀ x : P.FreeM α, x.bind (pure ∘ f) = map f x
184+
| .pure a => rfl
185+
| .liftBind a cont => by simp only [FreeM.bind, map, bind_pure_comp]
186+
187+
@[simp]
188+
lemma liftBind_bind (a : P.A) (cont : P.B a → P.FreeM β) (f : β → P.FreeM γ) :
189+
((FreeM.lift a).bind cont).bind f = (FreeM.lift a).bind (fun u ↦ (cont u).bind f) := by
190+
simp only [lift]
191+
exact FreeM.bind_assoc (FreeM.liftBind a pure) cont f
192+
193+
@[simp]
194+
lemma liftObj_bind (x : P.Obj α) (f : α → P.FreeM β) :
195+
(FreeM.liftObj x).bind f = FreeM.liftBind x.1 (fun a ↦ f (x.2 a)) := rfl
196+
197+
@[simp] lemma bind_eq_pure_iff (x : P.FreeM α) (f : α → P.FreeM β) (b : β) :
198+
x.bind f = pure b ↔ ∃ a, x = pure a ∧ f a = pure b := by
199+
cases x with
200+
| pure a => exact ⟨fun h => ⟨a, rfl, h⟩, fun ⟨_, h, hf⟩ => by cases h; exact hf⟩
201+
| liftBind a cont =>
202+
constructor
203+
· intro h
204+
cases h
205+
· rintro ⟨_, h, _⟩
206+
cases h
207+
208+
@[simp] lemma pure_eq_bind_iff (x : P.FreeM α) (f : α → P.FreeM β) (b : β) :
209+
pure b = x.bind f ↔ ∃ a, x = pure a ∧ pure b = f a := by
210+
cases x with
211+
| pure a => exact ⟨fun h => ⟨a, rfl, h⟩, fun ⟨_, h, hf⟩ => by cases h; exact hf⟩
212+
| liftBind a cont =>
213+
constructor
214+
· intro h
215+
cases h
216+
· rintro ⟨_, h, _⟩
217+
cases h
218+
219+
instance : Monad (P.FreeM) where
220+
221+
@[simp]
222+
theorem id_map : ∀ x : P.FreeM α, map id x = x
223+
| .pure a => rfl
224+
| .liftBind a cont => by
225+
simp only [map]
226+
congr 1
227+
funext u
228+
exact id_map (cont u)
229+
230+
theorem comp_map (h : β → γ) (g : α → β) :
231+
∀ x : P.FreeM α, map (h ∘ g) x = map h (map g x)
232+
| .pure a => rfl
233+
| .liftBind a cont => by
234+
simp only [map]
235+
congr 1
236+
funext u
237+
exact comp_map h g (cont u)
238+
239+
instance : LawfulMonad (P.FreeM) := LawfulMonad.mk'
240+
(bind_pure_comp := bind_pure_comp)
241+
(id_map := id_map)
242+
(pure_bind := pure_bind)
243+
(bind_assoc := FreeM.bind_assoc)
244+
245+
@[simp]
246+
lemma pure_inj (a b : α) : (pure a : P.FreeM α) = pure b ↔ a = b := by
247+
constructor
248+
· intro h
249+
cases h
250+
rfl
251+
· rintro rfl; rfl
252+
253+
lemma liftBind_inj (a a' : P.A)
254+
(cont : P.B a → P.FreeM α) (cont' : P.B a' → P.FreeM α) :
255+
FreeM.liftBind a cont = FreeM.liftBind a' cont' ↔ ∃ h : a = a', h ▸ cont = cont' := by
256+
constructor
257+
· intro h
258+
cases h
259+
exact ⟨rfl, rfl⟩
260+
· rintro ⟨rfl, rfl⟩
261+
rfl
262+
263+
section liftM
264+
265+
variable {m : Type uB → Type v} {α : Type uB}
266+
267+
/-- Interpret a `FreeM P` computation into any monad `m` by providing an interpretation
268+
`interp : (a : P.A) → m (P.B a)` for each operation. -/
269+
protected def liftM [Pure m] [Bind m] (interp : (a : P.A) → m (P.B a)) : P.FreeM α → m α
270+
| .pure a => pure a
271+
| .liftBind a cont => interp a >>= fun u ↦ (cont u).liftM interp
272+
273+
variable [Monad m] (interp : (a : P.A) → m (P.B a))
274+
275+
@[simp]
276+
lemma liftM_pure (a : α) : (Pure.pure a : P.FreeM α).liftM interp = Pure.pure a := rfl
277+
278+
@[simp]
279+
lemma liftM_lift_bind (a : P.A) (cont : P.B a → P.FreeM α) :
280+
FreeM.liftM interp (FreeM.lift a >>= cont) =
281+
(do let u ← interp a; (cont u).liftM interp) := by
282+
dsimp only [FreeM.liftM, FreeM.bind, FreeM.lift]
283+
rfl
284+
285+
/--
286+
A predicate stating that `eval : P.FreeM α → m α` is an interpreter for the polynomial
287+
effect handler `handler : (a : P.A) → m (P.B a)`.
288+
289+
This means that `eval` is a monad morphism from the free monad `P.FreeM` to the
290+
monad `m`, and that it extends the interpretation of individual operations given by
291+
`handler`.
292+
-/
293+
structure Interprets (handler : (a : P.A) → m (P.B a)) (eval : P.FreeM α → m α) : Prop where
294+
apply_pure (a : α) : eval (.pure a) = pure a
295+
apply_lift_bind (a : P.A) (cont : P.B a → P.FreeM α) :
296+
eval ((FreeM.lift a).bind cont) = handler a >>= fun x => eval (cont x)
297+
298+
theorem Interprets.eq {handler : (a : P.A) → m (P.B a)} {eval : P.FreeM α → m α}
299+
(h : Interprets handler eval) :
300+
eval = (·.liftM handler) := by
301+
ext x
302+
induction x with
303+
| pure a => exact h.apply_pure a
304+
| lift_bind a cont ih =>
305+
rw [h.apply_lift_bind]
306+
conv_rhs => simp only [bind_eq_bind, liftM_lift_bind]
307+
simp only [ih]
308+
309+
theorem Interprets.liftM (handler : (a : P.A) → m (P.B a)) :
310+
Interprets handler (·.liftM handler : P.FreeM α → _) where
311+
apply_pure _ := rfl
312+
apply_lift_bind _ _ := rfl
313+
314+
/--
315+
The universal property of the free monad `P.FreeM`.
316+
317+
That is, `liftM handler` is the unique interpreter that extends the effect handler `handler` to
318+
interpret `P.FreeM` computations in a monad `m`.
319+
-/
320+
theorem Interprets.iff (handler : (a : P.A) → m (P.B a)) (eval : P.FreeM α → m α) :
321+
Interprets handler eval ↔ eval = (·.liftM handler) :=
322+
⟨(·.eq), fun h => h ▸ Interprets.liftM _⟩
323+
324+
variable [LawfulMonad m]
325+
326+
@[simp]
327+
lemma liftM_bind {α β : Type uB} (x : P.FreeM α) (f : α → P.FreeM β) :
328+
(x >>= f).liftM interp = (do let u ← x.liftM interp; (f u).liftM interp) := by
329+
induction x with
330+
| pure _ => simp only [liftM_pure, LawfulMonad.pure_bind]
331+
| lift_bind a cont h =>
332+
simp_rw [bind_eq_bind]
333+
rw [LawfulMonad.bind_assoc, liftM_lift_bind]
334+
simp_rw [liftM_lift_bind, LawfulMonad.bind_assoc]
335+
congr 1
336+
funext u
337+
exact h u
338+
339+
@[simp]
340+
lemma liftM_map {α β : Type uB} (f : α → β) (x : P.FreeM α) :
341+
(f <$> x).liftM interp = f <$> x.liftM interp := by
342+
simp_rw [← LawfulMonad.bind_pure_comp, liftM_bind, liftM_pure]
343+
344+
@[simp]
345+
lemma liftM_seq {α β : Type uB}
346+
(interp : (a : P.A) → m (P.B a)) (x : P.FreeM (α → β)) (y : P.FreeM α) :
347+
(x <*> y).liftM interp = x.liftM interp <*> y.liftM interp := by
348+
simp [seq_eq_bind_map]
349+
350+
@[simp]
351+
lemma liftM_seqLeft {α β : Type uB}
352+
(interp : (a : P.A) → m (P.B a)) (x : P.FreeM α) (y : P.FreeM β) :
353+
(x <* y).liftM interp = x.liftM interp <* y.liftM interp := by
354+
simp [seqLeft_eq_bind]
355+
356+
@[simp]
357+
lemma liftM_seqRight {α β : Type uB}
358+
(interp : (a : P.A) → m (P.B a)) (x : P.FreeM α) (y : P.FreeM β) :
359+
(x *> y).liftM interp = x.liftM interp *> y.liftM interp := by
360+
simp [seqRight_eq_bind]
361+
362+
@[simp]
363+
lemma liftM_lift (interp : (a : P.A) → m (P.B a)) (a : P.A) :
364+
(FreeM.lift a).liftM interp = interp a := by
365+
simpa [bind_pure] using
366+
(liftM_lift_bind (interp := interp) (a := a) (cont := pure))
367+
368+
@[simp]
369+
lemma liftM_liftObj (interp : (a : P.A) → m (P.B a)) (x : P.Obj α) :
370+
(FreeM.liftObj x).liftM interp = x.2 <$> interp x.1 := by
371+
simp [liftObj]
372+
373+
end liftM
374+
375+
end FreeM
376+
377+
end PFunctor

0 commit comments

Comments
 (0)