As a library, «freely» attempts to realize the slogan "free(r) syntax, modular semantics". It attempts to achieve this by equipping signatures / protocols written in continuation-passing style (CPS) with their initial interpreters, using the free(r) monad, so that meaning is stable (essentially unique), compositional (monadic), and portable (functorial).
The idea, roughly speaking, is that:
programs should be written using free(r) monads (Freer «Σ» α, i.e. ℙ «Σ» α) over (DSL) signatures «Σ»)
(DSL) signatures«Σ» should be functorial expressions of the syntax of a domain specific language (e.g. «Σ_Foo», «Σ_Bar»,«Σ_Foo⊕Bar» etc.), so that they admit of composition and combination via functor combinators
interpretations should be realized as canonical folds/catamorphisms∀ {α}, ℙ «Σ» α → m α that employ handlers «Σ» ⟹ᶠ m to interpret programs ℙ «Σ» α in monadic execution contexts.
Under such an arrangement, composing DSLs and optimizing interpretations of programs can be done using standard categorical constructions, rather than bespoke machinery, or mechanisms tightly bound to any specific domain.
1.5. «freely», at a glance (where to start)
When read in order (top-down), this document presents «freely» in a continuous narrative, starting with the motivation driving the creation of the library, the notation used in «freely» (and throughout this document), and «freely»'s relationship to prior art, before diving into its category theoretic grounding, and finally presenting a thorough, module-by-module presentation of its code.
But that is only one way to read this document. This document can be read in many different orders, depending upon pedogogical purposes, and personal interests. So please feel free to skip around!
If you're eager to just dive into the core code abstractions behind the library, you might begin with:
«freely-log» establishes a logging DSL and handlers, while «freely-tests» reuses the logging DSL and handlers in the course of building a tests DSL and handlers, which it then uses to write tests for «freely» itself. Both «freely-log» and «freely-tests» give an initial flavor of what it's like to use the «freely» abstractions to build DSL, handlers, and interpreters, and to compose them into larger DSLs, etc.
If you're eager to see tests witnessing both usage of freely abstractions and laws governing (algebraic) reasoning, see:
Finally, we have attempted to link to nLab where appropriate when categorical notions are introduced. We encourage readers unfamiliar with such notions to follow these links to get a better appreciation of the generality and scope of the notions involved, understanding that any link to nLab is a link to a rabbit hole of potentially infinite depth.
1.6. Motivation behind «freely»
We understand our goal to be to enable "free(r) expression" by offering avenues for leveraging "freer syntax" and "modular semantics". This must entail (at a minimum) the ability:
to author programs against many different (domain specific) languages
to be able to compose these languages, permitting their extension, their embedding in larger languages, and the like
to be able to reuse programs written in these languages, allowing interpretations of higher languages to delegate to interpretations of lower languages, etc., and
to be able to defer the meaning of programs until interpretation (permitting freely replacing, substituting, and extending interpretations)
Free(r) expression must always contend with the fundamental obstacle to communication we always face: viz. that you don't already know what I mean. To communicate, I must send you (serialized) data, and you must interpret that (de-serialized) data in some way that you presume coheres with my intended meaning.
If we separate syntax (data) from semantics (interpretations of data) in a principled way that anticipates their structured recombination, then we can presumably express what it is that we mean in a way that supports advanced reasoning and optimization of programs and their interpretations.
The «freely» library models (domain-specific) languages by endofunctors «Σ» : Type u → Type u (effect signatures). When one builds programs as free objects over «Σ» (sequential programs via a free(r) monad, or - planned but not yet reified in code - parallel programs via a free(r) applicative), one can then give expression(s) in languages semantic meaning by defining handlers from the languages into monadic contexts, i.e. by defining natural transformationsθ : «Σ» ⟹ᶠ m. The «freely» library supplies folds/catamorphisms, as well as various combinators, so that composition, extension, and rewriting follow from universal properties and naturality conditions.
write programs purely against Freer (a.k.a. ℙ «Σ») for sequential programs, or (planned, not yet implemented) FreeA «Σ» (for parallel programs)
supply handlers θ : «Σ» ⟹ᶠ m to answer questions, i.e. to supply the semantic meaning of expressions employed in programs
Under this approach, programs involve elaborating expressions that you may think of as asking questions that are meant to be answered by handlers. Programs are entirely syntactic constructs, and are independent from their interpretation(s). Crucially, just as DSLs can be composed, handlers can be composed too. Interpretation provides a canonical way to "run" a handler, and because it is a catamorphism / fold, any algebra we put on handlers is preserved through this fold.
1.8. How is this "free(r)" expression?
In its fullest expression, a guiding goal of «freely» is to enable us to write programs once, and reuse them myriad times, to perform radically different effects. Separating the syntactic layer from the semantic layer allows us to manipulate both algebraically, and to share highly reusable programmatic logic, pushing (substitutable, algebraically manipulable) handlers to the boundaries of the program. Further, because DSLs can be composed, programs written in one language can be lifted / embedded into larger languages. A high level language's semantics can involve interpreting programs written in a lower level language, and the functorial combinatorics of how languages and handlers compose is available to us.
In sum, though there are a great many disparate goals that the «freely» library aims to support, most of them invariably revolve around freely using, reusing, composing, and substituting syntactic and semantic constructs to facilitate conveyance of meaning in a composable, reusable way.
1.9. Disclaimer
This is experimental software. «freely» is in its infancy. I am a (principal) software engineer with experience in Haskell, functional programming, and retail pricing, first and foremost - as well as a student of philosophy - but I am not a mathematician, nor a category theorist. Consider this work a modest submission in the vein of "applied" category theory, understanding that those of us (like me) who attempt such an application are sometimes in need of guidance and correction on mathematical topics that exceed our training. In other words: caveat emptor.
If you find errors in reasoning and the like, please notify me on Zulip, or create a Github issue in the «freely» repo.
For more information on the author, see this section at the bottom of this document.
2. Notation in «freely»
Concept
Level
Notation
Type
signature (of DSL)
functor
«Σ», «Σ_Foo», «Σ_Bar», «Σ_Foo⊕Bar», etc.
«Σ» : Type u → Type u with [Functor «Σ»]
sum (of DSLs)
functor
«Σ_Foo⊕Bar» as «Σ_Foo» ⊕ᶠ «Σ_Bar»
«Σ_Foo» ⊕ᶠ «Σ_Bar» : Type u → Type u with [Functor «Σ_Foo» ⊕ᶠ «Σ_Bar»]
freer monad on a signature «Σ»
functor / monad
ℙ «Σ»
ℙ «Σ» : Type u → Type u with [Monad (ℙ «Σ»)] (alias Freer «Σ»)
natural transformation («Σ»-algebra in M)
functor → monad
α : «Σ» ⟹ᶠ M
Σ ⟹ᶠ M ≔ ∀ {α}, Σ α → M α with naturality (map f law); «Σ» : Type u → Type u, M : Type u → Type u
handler (wrapping «Σ»-algebra in M)
algebra-in-M
θ : «Σ» ⟹ᶠ M
θ : ∀ {α}, «Σ» α → M α, natural in α (aliased Handler «Σ» M)
left/right injections
functor
←ᶠ, →ᶠ
←ᶠ : F ⟹ᶠ (F ⊕ᶠ G); →ᶠ : G ⟹ᶠ (F ⊕ᶠ G)
member/injection into «Σ»
functor
Member F R
[Member «Σ_Foo» «Σ_Baz»] providing inj : «Σ_Foo» ⟹ᶠ «Σ_Baz» (often built from ←ᶠ/→ᶠ and sums)
Carrier A : Type u, structure map a : M A → A with laws a ∘ pure = id, a ∘ join = a ∘ map a
3. Prior Art
It should be stated up-front that «freely» does not implement a novel idea. It is, instead, an attempt to elaborate ideas that have been articulated by many before, and will be articulated by many more in the future.
One might rightly view «freely» as a mashup of the idea of free(r) structures, combined with the idea of combinators on functors. Particularly noteworthy are Kiselyov and Ishii's Freer Monads, More Extensible Effects, and the myriad libraries inspired by this approach (many of which are listed below), as well as Justin Le's functor-combinators.
To be clear: I am not a scholar with an academic background in this literature; so the below should not be regarded as a comprehensive literature review. Instead, I have only listed those papers and libraries that I am personally aware of, that I believe have some (possibly tangential) relevance to the current work.
If you think this list / discussion of prior art should be amended in some way, please contact me or submit an issue with the correction.
Handler «Σ» m ≅ «Σ» ⟹ᶠ m; fold ↓ᵀ yields the unique monad morphism interpret
Interpreters run… per effect; naturality by parametricity
Same idea; interpret/reinterpret combinators
Carriers/final style; algebraic laws explicit in types
Core encoding
CPS freer Freer «Σ»
Open-union + continuation-based Eff r
CPS freer (Eff r)
Different encodings (carriers/final)
3.2. Prior art around Functor Combinators
Freely (Lean 4)
Functor-combinator (Haskell)
Intuition
Notes / Status
«Σ» : Type u → Type u
f :: * -> *
A vocabulary of asks / operations
Constructors may be CPS-encoded
«Σ₁» ⊕ᶠ «Σ₂»
Sum f g / :+:
Choose one of two asks
Right-associated; can use Member for injection
Freer «Σ» (aka ℙ «Σ»)
Free f
Sequential programs (terms)
CPS freer; isomorphic to Free
«Σ» ⟹ᶠ m, Handler «Σ» m
Natural transformation f ⟹ m
Semantics for each ask
Componentwise naturality (map law)
zeroHandler, ⊕ᵃ
empty / pointwise (<|>)
Ignore / try-left-else-right
Pointwise lifting of Alternative-like structure
⊗ᵃ (handler product)
Lifted product via Applicative
Run two handlers together
Induced from applicative strength
(planned, not yet implemented) FreeA «Σ»
FreeA f
Parallel/batchable programs
Marked "planned" in docs
(planned, not yet implemented) ⊗ᶠ (Day tensor)
Day f g
Two asks "in parallel", combine results
Preserves functorial structure; monoidal on signatures
(planned, not yet implemented) 𝟙ᶠ
Identity
Neutral "do nothing" ask
Unit for Day
(planned, not yet implemented) ProdF
Product f g / :*:
Keep both payloads, no mixing
Useful for paired annotations
(planned, not yet implemented) ComposeF
Compose f g
Nested asks
For staged syntax
(planned, not yet implemented) ConstF w
Const w
Accumulate metadata (weights, cost, …)
Monoid-indexed decorations
In truth, the aim is to extend the idea even further than this, as far as we can take it:
to take syntax / data structures, and lift to the functor level
to unlock algebraic ways to combine expressions, embed expressions, rewrite expressions, and perform substitution on expressions, and
to take the semantic handlers of languages and compose them, chain them, replace them, and so on.
In other words, we aim to utilize and structure the relationship between syntax and semantics so as to freely aid the conveyance of meaning, however articulated.
Just what is one to do with extremely abstract concepts?
If nothing else, «freely» may be of use to those who wish somehow to ground the real, computable world, in the abstract world of categorical concepts and abstractions, and vice versa.
4. Universes, and the ambient category
The categorical reasoning grounding this library operates in a category whose objects are types in some universe level u, and whose morphisms are functions. This choice is categorically natural, universe-safe, and amenable to computation.
Some authors, including Dusko Pavlovic, reify a similar notion as the category Par - the category where objects are types and whose arrows are partial computable functions. (See Pavlovic's "Programs as Diagrams").
The important thing to note about such a category is that, although it is not itself cartesian-closed, it has a core that is cartesian-closed: viz. that sub-categorical core which consists of types as objects, and arrows as total computable functions. This allows us to speak sensibly of initial and terminal objects, finite products / coproducts, and exponential terms.
4.1. Universe levels and smallness
Lean has a cumulative hierarchy
\mathbf{Type}_0 \;\subseteq\; \mathbf{Type}_1 \;\subseteq\; \mathbf{Type}_2 \;\subseteq\; \cdots
\quad\text{with}\quad \mathbf{Type}_u : \mathbf{Type}_{u+1}
The bare identifier Type is syntactic sugar for Type 0.
Fixing 𝒞 = Type (i.e. Type 0) can sometimes prove to be too small. Generic constructions (free monads, handlers, etc.) may sometimes need to live at an arbitrary higher level.
Thus, we work universe-polymorphically with a parameter u and set 𝒞 := Type u.
4.1.1. Practical rule
We always declare universes, and keep all core signatures and constructions at the same level:
This ensures that the signature «Σ», the free monad ℙ«Σ», and the semantic monads M are endo-morphisms on the same base category 𝒞 := Type u (which we may also write in this section and others as \mathbf{Type}_u).
4.2. Type u has a cartesian-closed core with finite coproducts
Equipped with functions as morphisms, the core of 𝒞 := Type u (a.k.a. \mathbf{Type}_u) has:
In a cartesian closed category the canonical monoidal structure is the cartesian one, so the tensor is literally the categorical product: ((\mathcal C, \times, 1)) is symmetric monoidal. It is traditional to write × to signal “product” (with its projections and universal cone), reserving ⊗ for a generic monoidal tensor that need not be cartesian. We regard ((\mathbf{Type}_u, \times, \mathbf{1})) as a symmetric monoidal category (cartesian monoidal), so that the "tensor" coincides with cartesian product; but we keep the symbol × here to avoid confusion with the tensor ⊗ᶠ on signatures.
This will allow us to meet the hypotheses used in the rest of the abstract presentation of the library: viz. the presentation in terms of polynomial endofunctors, free monads, and strengths/distributivity laws.
4.3. Endofunctors, free monads, and handlers at universe level u
With 𝒞 := Type u fixed, a signature is an endofunctor «Σ» : Type u → Type u. Additionally, the free monad ℙ«Σ», handlers into semantic monads (M : \mathcal C \to \mathcal C), and interpreters are all also endomorphisms on \mathbf{Type}_u. No universe shifts are introduced by the core API.
4.4. Why not bare Type?
Type means Type 0. Restricting ourselves in this way would needlessly forbid using signatures or semantic monads that live in a higher universe (e.g. when data mentions large indices). We would then be forced into ULift plumbing or ad-hoc casts.
Using Type u makes every theorem and definition universe-polymorphic, so they instantiate uniformly at any desired level.
4.5. «freely» design contract
We fix a universe u and set ( \mathcal C := \mathbf{Type}_u ).
We work with DSL signatures that are endofunctors ( «Σ» : Type u → Type u ).
We construct the free monad ( ℙ «Σ» ) on the same (\mathcal C).
We construct handlers from «Σ» into M with M of type \mathcal C \to \mathcal C.
We use the initiality of ((\mathbb T,\alpha)) in the category Mnd^(«Σ») of such pairs to obtain a unique interpreter.
All categorical claims in the «freely» manual (polynomials, sums, initiality, commuting squares) specialize to the concrete model ( \mathcal C = \mathbf{Type}_u ), while remaining universe-polymorphic.
5. Introduction to the categorical viewpoint
5.1. An overview of the categorical picture pursued in «freely»
Let 𝒞 = Type u and let a signature be an endofunctor «Σ» : 𝒞 ⟶ 𝒞.
The core idea is that ℙ «Σ» is initial among monads m with a natural transformation «Σ» ⟹ᶠ m, and that our catamorphism / fold (cataFreer) should realize the unique monad morphism. This is what the Freer monad instance should encode operationally.
Freely.Freer.{u,v}(F:Type u→Type v)(α:Type u):Type (max (u + 1) v)
Freely.Freer.{u,v}(F:Type u→Type v)(α:Type u):Type (max (u + 1) v)
Freer F (notation: ℙ F) — the free monad on a mapping F : Type u → Type v.
Mathematically, FreerF is the initial object among monads M equipped with
a natural transformation F⇒M. Operationally it is the syntax of programs
freely built from F-effects and pure.
pure:α→ℙFα injects a return value.
freer:Fβ→(β→ℙFα)→ℙFα is the “one effect then continue” step.
Universe: if F:Typeu→Typev, then ℙF:Typeu→Type(max(u+1)v).
Monad unit of the CPS freer monad.
Categorical reading. The unit ηᵀ:Id⇒T for T=FreerΣ, inserting a
variable as a leaf (no Σ-node). Laws (as monad morphism obligations for the
interpreter): interpret∘pure=pure, and bind(purex)k=kx.
Categorical reading. This is the “node + continuation” constructor whose
catamorphism realizes the constructor algebra α:ΣT⇒T under a CPS encoding.
Schematic type: freer:Σβ→(β→FreerΣα)→FreerΣα
It underlies the one-layer inclusion liftFreer:Σα→FreerΣα.
The component at α of the natural transformation θ:«Σ»⟹ᶠM.
is a componentwise natural transformation «Σ» ⟹ᶠ m (parametric in β), ensuring the naturality square. Folding by ↓ᵀ handle gives the unique ℙ «Σ» ⟹ᶠ m.
Interpret a ℙ«Σ»α program into mα, using a handler θ:«Σ»⟹ᶠm argument.
This is akin to interpret but accepts the handler as an argument instead of doing
an instance search.
interpret'θ is the free extension of a handler θ:«Σ»⟹ᶠm: the unique monad morphism
ℙ«Σ»⇒m such that interpret'θ∘↑ᵀ=θ
⟦θ⟧ is natural in α and preserves the monad structure:
unit: ⟦θ⟧ (ηᵀ a) = pure a (your interpret'_pure)
multiplication/bind: ⟦θ⟧ (x >>= k) = ⟦θ⟧ x >>= (fun a => ⟦θ⟧ (k a))
Freely.SumF.{u}(FG:Type u→Type u)(α:Type u):Type u
Freely.SumF.{u}(FG:Type u→Type u)(α:Type u):Type u
SumF, written F⊕ᶠG, is the binary coproduct of endofunctors on Typeu.
This is meant to denote a sum of signaturesk the additive combinator on vocabularies of asks.
realises the unique injection of a summand F into a right-associated sum R = G₁ ⊕ … ⊕ Gₖ. This is the functor-level analogue of the Haskell “open union row” (Member eff r) used by libraries such as extensible-effects and freer-simple.
MemberFR says that F can be embedded as a summand of R.
Instance Constructor
Freely.Member.mk.{u}
Methods
inj : {α:Type u}→Fα→Rα
Canonical injection of a summand into a right-associated signature sum.
Given [MemberFR], the natural transformation inj:F⟹ᶠR is the unique
functorial inclusion determined by the right-spine decomposition of R.
Operationally, sendᵀ := (↑ᵀ) ∘ inj : F ⟹ᶠ ℙ R.
by composing Member.inj with ↑ᵀ. (In the Haskell ecosystem this role is often covered by Template Haskell splices such as freer-simple’s makeEffect or polysemy’s makeSem that generate send-style functions from GADT effect signatures.)
You may think of «freely» as having in mind two parallel rigs, which it attempts to reify (note: free applicative, tensors, etc. are planned, but have not yet been implemented):
a rig on handlers: (⊕ᵃ, ⊗ᵃ; zeroHandler, oneHandler) (semantics)
with the idea that there should be some reasonable assumptions / cases where these rigs are preserved under interpretation / catamorphism.
6. Deepening the categorical picture
6.1. Ambient setting
Let 𝒞 be a cartesian closed category (CCC) with finite products and finite coproducts. When thinking operationally about Lean, we'll let 𝒞 ≅ Type u.
Formally (in logic) a signature Σ essentially consists of
a set of sorts or types
a set of relation symbols
a set of function symbols
along with functions defining the arity of the above symbols.
The concept of a signature Σ allows us to formally describe the syntactic presentation of any fragment of (first order) logical language (inter alia).
So if we have a domain specific language (DSL) in mind, we can speak of the signature Σ of that DSL by formally defining the sorts / types, relations, and function symbols permitted in valid expressions written in that DSL.
Mathematically, a finitary signature Σ determines a polynomial functor: F_\Sigma(X)=\sum_{o\in O} X^{A_o}. This expresses that a signature is a sum over operations o∈O of exponentials X^(Aₒ) of some (finite) arity Aₒ, in a way that is parametric in X.
In «freely», we simply name that functor as «Σ» : Type u → Type u. So for the purposes of this manual, you can read F_\Sigma as equivalent to «Σ». Signatures are implemented in a CPS‑polynomial style, so that operation nodes carry their continuation. (This is an intentional design choice - one that we think is desirable because it will end up making distributivity / strength coherence automatic for folds / catamorphisms.)
6.2. The free monad on a signature
In what follows, we define a carrier functor T := ℙ «Σ» in order to name the functor part of ℙ «Σ» as T, so that e.g. we can write maps in standard mathematical form like α : «Σ» T ⇒ T.
Given «Σ», there exists a free monad on «Σ». Namely, we define ℙ «Σ» = (T, η, μ, ι), where:
T : 𝒞 → 𝒞 is the underlying (carrier) functor of the monad.
η : Id ⇒ T is the unit operation
μ : T ∘ T ⇒ T is the multiplication operation, and
ι : «Σ» ⇒ T is the universal inclusion operation
This monad is initial among monads equipped with such an ι.
Given «Σ», the free monadℙ «Σ» = (T ,\eta, \mu, \iota) consists of
a carrier functor ( T : \mathcal C \to \mathcal C ),
unit ( \eta : \mathrm{Id} \Rightarrow T ),
multiplication ( \mu : T \circ T \Rightarrow T ),
and a universal inclusion of generators ( \iota : «Σ» \Rightarrow T ).
Intuitively, T X can be regarded as the type / object of «Σ»-terms with variables in X.
The free monad also carries its canonical one‑step constructor algebra \alpha : \Sigma T \Rightarrow T defined by \alpha = \mu \circ \iota = \mathrm{join} \circ \mathrm{liftFreer}.
In code, \alpha is not stored, but it is definable from liftFreer and join.
Given a handler (algebra) algebra:∀{β:Typeu},Fβ→mβ for some monad m,
cataFreeralgebra:ℙF⇒m is the unique monad morphism that:
sends purex to purex, and
runs one F-node at a time via algebra, then continues with the residual
program.
Termination: for a finite Freer value and a total/non-diverging
handler algebra, this fold terminates after one call to algebra per freer node.
6.3. Target semantics: (M, θ)
A target monad with handler is a pair (M, θ) where
M : 𝒞 → 𝒞 is a monad (the semantic/target monad), and
θ : «Σ» M ⇒ M is a handler (a natural transformation giving one-step «Σ»-semantics in M).
Handler «Σ» M with handle : «Σ» α → M α (also written «Σ» ⟹ᶠ M).
Equivalent categorical views:
As an object of Mnd^(«Σ» ): an object is exactly (M, θ); morphisms are monad morphisms preserving θ.
Via distributive law/strength: a law «Σ» M ⇒ M «Σ» lifts «Σ» to Kleisli/Eilenberg-Moore; then θ is a «Σ»-algebra internal to M, and the interpreter defined below is the initial morphism from the free algebra/monad
6.4. Interpreter, and the universal commuting law
Given (M, θ), there exists an interpreter
⟦-⟧_θ : T ⇒ M
which is the unique monad morphism satisfying the commuting law
Interpret a ℙ«Σ»α program into mα, using a handler θ:«Σ»⟹ᶠm argument.
This is akin to interpret but accepts the handler as an argument instead of doing
an instance search.
interpret'θ is the free extension of a handler θ:«Σ»⟹ᶠm: the unique monad morphism
ℙ«Σ»⇒m such that interpret'θ∘↑ᵀ=θ
⟦θ⟧ is natural in α and preserves the monad structure:
unit: ⟦θ⟧ (ηᵀ a) = pure a (your interpret'_pure)
multiplication/bind: ⟦θ⟧ (x >>= k) = ⟦θ⟧ x >>= (fun a => ⟦θ⟧ (k a))
«Σ» T --α-------> T
| |
| «Σ»(⟦-⟧_θ) | ⟦-⟧_θ
v v
«Σ» M --θ-------> M
6.5. Category of «Σ»-interpreters and initiality
Define Mnd^(«Σ»), the category whose:
Objects are pairs (M, θ) with θ : «Σ» M ⇒ M.
Morphisms are monad morphisms φ : M ⇒ N preserving handlers:
φ ∘ θ_M = θ_N ∘ «Σ»(φ) .
(T, α) is an object of Mnd^(«Σ»), initial among such objects.
6.6. Modularity: sums, membership, copairing
Signature sum
SumF «Σ₁» «Σ₂»
«Σ₁» ⊕ᶠ «Σ₂»
Injections
inl / inr
←ᶠ , →ᶠ
Membership
F ⊂ R
Member F R with inj : F ⇒ R
Handler copairing
[θ₁, θ₂]
[«Σ₁» ⟹ᶠ M], [«Σ₂» ⟹ᶠ M] ⟹ («Σ₁» ⊕ᶠ «Σ₂») ⟹ᶠ M
We support:
handler zero/plus 0, ⊕ᵃ ↔ zeroHandler, ⊕ᵃ
(planned - not yet implemented) signature tensor ⊗ᶠ via Day ↔ Tensor
(planned - not yet implemented) handler product ⊗ᵃ via Applicative/Day-lift)
6.7. Minimal hypotheses (what is strictly required)
𝒞 has finite products/coproducts (to form polynomial «Σ»).
The free monad ℙ «Σ» exists.
M admits of strength/distributive-law coherence so that θ : «Σ» M ⇒ M is well-typed and natural.
In code, we achieve (1) and (2) via ℙ «Σ» + MonadLaws. (3) is ensured by CPS signatures and cataFreer, with naturality/monad-morphism properties tested in Naturality.lean and Interpretation.lean.
Our signatures are implemented in a CPS-polynomial style (operation nodes carry continuations), so a handler already returns an M-action for the continuation. In this presentation, the coherence typically packaged as a distributive law is inherent in the typing of the fold. The library still exposes and tests naturality and the interpreter laws, and the initiality statement remains exactly as above.
This earns us:
Canonical meaning: fixing θ yields a unique ⟦-⟧_θ.
Functorial transport: if φ : M ⇒ N preserves handlers, then φ ∘ ⟦-⟧_θ = ⟦-⟧_{θ’}.
Compositionality by construction: evaluation is an algebra homomorphism; substitution is μ.
Modularity and extension: extending «Σ» via sums extends programs and handlers modularly.
Algebraic location: sums/products live in the syntax shape («Σ»), not in ad-hoc code.
6.8. Two categorical viewpoints: Kleisli and Eilenberg–Moore
6.8.1. Kleisli viewpoint
Composition in M is Kleisli composition. A monad morphism \llbracket - \rrbracket_\theta : T \Rightarrow M induces a functor \mathrm{Kl}(\llbracket - \rrbracket_\theta) : \mathrm{Kl}(T) \longrightarrow \mathrm{Kl}(M), implemented by (f:X \to T Y) \longmapsto \big(\llbracket - \rrbracket_\theta\big)_Y \circ f : X \to M Y
Running programs in the target semantic context M therefore simply involves post‑composition by interpret θ, and it preserves identities/composition because interpret is a monad morphism.
6.8.2. Eilenberg–Moore viewpoint
A handler \theta : \Sigma M \Rightarrow M is a \Sigma-algebra internal to M, i.e. an algebra for the lift of \Sigma to the EM category \mathcal C^M. Picking an M-algebra a : M A \to A yields a total (as opposed to partial) semantics
\mathrm{eval}(a,\theta) : T A \xrightarrow{\ \llbracket - \rrbracket*\theta\ } M A \xrightarrow{\ a\ } A
that observes/collapses effects algebraically.
7. Freely.Algebra.ZeroPlus — additive structure for targets & handlers🔗
This module is about the codomain (monads/applicatives we interpret into) and the pointwise algebra on handlers (e.g., zero/plus via Option, ReaderT, etc.).
This equips our codomain with the algebra needed for choice and “try A else B” composition. The two typeclasses:
ZeroM m — a distinguished “no result”/annihilator in m
Combine two computations; prefer the left when meaningful.
capture the additive half of our calculus. In other words, we build to:
a zero handler that always fails/silences (zeroHandler),
pointwise addition of handlers (θ ⊕ᵃ φ), and
rewrite lemmas showing that interpretation ↓ᵀ preserves this structure
(so we can reason equationally about swapping handlers, masking, routing, etc.).
This module is all about setting us up to configure semantics by
algebra on handlers. (Forthcoming: distributivity laws when
we add the multiplicative side, i.e. applicative/tensor/etc.)
/-!# Freely.Algebra.ZeroPlus — additive structure for targets & handlers
-/namespaceFreelyuniverseu/-- A codomain with a distinguished "no result". Typical instances: `Option`. -/classZeroM(m:Typeu→Typeu)extendsMonadmwhere/-- The failed/empty computation. -/empty:∀{α},mαinstance:ZeroMOptionwhereempty:=noneinstance{m:Typeu→Typeu}{ρ:Typeu}[ZeroMm]:ZeroM(ReaderTρm)whereempty:=λ_=>ZeroM.emptyinstance{m:Typeu→Typeu}{σ:Typeu}[ZeroMm]:ZeroM(StateTσm)whereempty:=λ_=>ZeroM.empty/-- A codomain with a left-biased choice. Typical instances: `Option` (`orElse`),
`Sum ε` (prefer `ok`), lists (concatenate), and writer-like monads. -/classPlusM(m:Typeu→Typeu)extendsZeroMmwhere/-- Combine two computations; prefer the left when meaningful. -/orElse:∀{α},mα→mα→mα/-- Notation for pointwise addition on handlers/codomains. -/infixl:55" ⊞ "=>PlusM.orElseinstance:PlusMOptionwhereorElsexy:=x.orElse(fun_=>y)instance{m:Typeu→Typeu}{ρ:Typeu}[PlusMm]:PlusM(ReaderTρm)whereorElsemxmy:=λr=>PlusM.orElse(mxr)(myr)instance{m:Typeu→Typeu}{σ:Typeu}[PlusMm]:PlusM(StateTσm)whereorElsemxmy:=λs=>PlusM.orElse(mxs)(mys)endFreely
importFreely.Algebra.ZeroPlusimportFreely.Handler.CoreimportFreely.Program.Monad.FreernamespaceFreelyuniverseu/-- Annihilating handler: ignores every operation, returns `empty`. -/defzeroHandler(«Σ»m:Typeu→Typeu)[ZeroMm]:«Σ»⟹ᶠm:=⟨fun{_α}_=>ZeroM.empty⟩/-- Pointwise “addition” of handlers using `m`’s `orElse`. -/defhPlus{«Σ»m}[PlusMm](θφ:«Σ»⟹ᶠm):«Σ»⟹ᶠm:=⟨fun{_α}op=>PlusM.orElse(θ.handleop)(φ.handleop)⟩infixl:60" ⊕ᵃ "=>hPlus/-- `[0, φ]` — annihilate the `F`-side, handle the `G`-side with `φ`. -/defmuteLeft{FGm}[ZeroMm](φ:G⟹ᶠm):(F⊕ᶠG)⟹ᶠm:=copair(zeroHandler(F)(m))φ/-- `[θ, 0]` — handle the `F`-side with `θ`, annihilate the `G`-side. -/defmuteRight{FGm}[ZeroMm](θ:F⟹ᶠm):(F⊕ᶠG)⟹ᶠm:=copairθ(zeroHandler(G)(m))endFreely
9. Freely.Handler.Core — natural transformations as effect handlers🔗
This module introduces handlers: the way a pure question (an operation from a signature
Σ) gets its meaning in some target effect m. Think of a handler as a natural
transformationΣ ⇒ m: for each result type α it maps a question Σ α to an answer
m α, uniformly in α.
All “policy” and “assumption” choices live here.
Programs (written against Freer Σ or FreeA Σ) only ask; handlers decide how
to interpret those asks: from oracles, from models, with fallbacks, with logging, etc.
Because handlers are just natural transformations, we can compose and reason
algebraically (sums of signatures, zero/plus, products, masks/routers) and push equalities
through interpretation / catamorphism (↓ᵀ).
This anticipates equipping handlers with:
an additive structure (zero handler + pointwise choice ⊕ᵃ),
a multiplicative structure via the tensor on signatures and the Applicative product
in the codomain (⊗ᵃ, with a unit handler),
so that we can normalize, batch, and factor analyses equationally.
A handler «Σ» ⟹ᶠ m is a natural transformation that interprets one node
of «Σ» into the codomain m. Catamorphism (↓ᵀ) uses handlers to run programs.
In other words, handlers are the algebras used by ↓ᵀ (cataFreer) to
interpret programs ℙ Eff into m.
If you can handle F and G individually, you can handle their coproduct: Handler (F ⊕ᶠ G) m is derived by case analysis and delegating to each side.
importFreely.Signature.SumnamespaceFreely/-!# Freely.Handler.Core — natural transformations as effect handlers
-/universeuvwvariable(F:Typeu→Typeu)(G:Typeu→Typeu)/-- A natural transformation `«Σ» ⟹ᶠ m` (component-wise `«Σ» β → m β`). -/classHandler(«Σ»:Typeu→Typeu)(m:Typeu→Typeu)where/-- The component at `α` of the natural transformation `θ : «Σ» ⟹ᶠ M`. -/handle:∀{β:Typeu},«Σ»β→mβ/-- Notation for a handler / natural transformation. -/scopedinfixr:65" ⟹ᶠ "=>Handler/-- Handlers compose over coproducts: if `F ⟹ᶠ m` and `G ⟹ᶠ m`,
then `(F ⊕ᶠ G) ⟹ᶠ m`. -/instance{m:Typeu→Typeu}[F⟹ᶠm][G⟹ᶠm]:(F⊕ᶠG)⟹ᶠmwherehandle|←ᶠf=>Handler.handlef|→ᶠg=>Handler.handleg/-- Copairing of handlers. Given `θ₁ : «Σ₁» ⟹ᶠ M` and `θ₂ : «Σ₂» ⟹ᶠ M`, produce
`[θ₁, θ₂] : («Σ₁» ⊕ᶠ «Σ₂») ⟹ᶠ M` with the defining equations
`[θ₁, θ₂] ∘ inl = θ₁` and `[θ₁, θ₂] ∘ inr = θ₂`. -/defcopair{FGm:Typeu→Typeu}(θF:F⟹ᶠm)(θG:G⟹ᶠm):(F⊕ᶠG)⟹ᶠm:=⟨fun{_α}x=>matchxwith|←ᶠf=>θF.handlef|→ᶠg=>θG.handleg⟩/-- Handler for a sum of signatures, implemented as copairing of component handlers. -/defhandleSum{FGm:Typeu→Typeu}(θF:F⟹ᶠm)(θG:G⟹ᶠm):(F⊕ᶠG)⟹ᶠm:=copair(θF:F⟹ᶠm)(θG:G⟹ᶠm)/--For many summands we can nest `handleSum`, but the scalable pattern is
a typeclass that supplies a handler for each signature and lifts along sums.
-/classHandles(«Σ»:Type→Type)(m:Type→Type)where/-- `θ : «Σ» ⟹ᶠ M` (a natural transformation from the signature to the semantic monad). -/θ:«Σ»⟹ᶠmexportHandles(θ)/-- Recursive instance for sums (co-pairing). -/instancehandlesSum{FGm}[HandlesFm][HandlesGm]:Handles(F⊕ᶠG)m:=⟨handleSum(θ(«Σ»:=F)(m:=m))(θ(«Σ»:=G)(m:=m))⟩endFreely
10. Freely.Handler.Interpret — folding free programs with a handler🔗
This module provides the catamorphism from the free monad Freer «Σ» (ℙ F) into a target
effect m: given a handler θ : «Σ» ⟹ᶠ m, it interprets a pure program
ℙ «Σ» α into m α.
Interpret a ℙ«Σ»α program into mα, using a handler θ:«Σ»⟹ᶠm argument.
This is akin to interpret but accepts the handler as an argument instead of doing
an instance search.
interpret'θ is the free extension of a handler θ:«Σ»⟹ᶠm: the unique monad morphism
ℙ«Σ»⇒m such that interpret'θ∘↑ᵀ=θ
⟦θ⟧ is natural in α and preserves the monad structure:
unit: ⟦θ⟧ (ηᵀ a) = pure a (your interpret'_pure)
multiplication/bind: ⟦θ⟧ (x >>= k) = ⟦θ⟧ x >>= (fun a => ⟦θ⟧ (k a))
interpret_pure says interpretation is identity on pure.
interpret_freer is the “one-step” equation: interpret a single operation by θ,
then continue by recursion. Together they characterize interpretation.
All the equational stories (“zero annihilates,” “choice distributes,”
“mask ≡ filter”, “traverse fusion”) are proofs by fold: we can reduce programs by these
equations, and reason about handlers algebraically.
The sibling interpretA in FreeA does the same for applicative
programs, which unlocks parallel/independent composition (batching and static analyses).
interpret is the generic interpreter for free(r) monadic programs:
given a handler θ : «Σ» ⟹ᶠ m, it is exactly the catamorphism ↓ᵀ θ.handle.
This is the canonical way to run a freer program:
handle one «Σ»-node via handle, obtain the next seed, and recurse.
Stated again: given a handler «Σ» ⟹ᶠ m, the canonical runner for a freer program involves interpreting each node with handle, obtaining the continuation seed, and recursing.
importFreely.Program.Monad.FreerimportFreely.Handler.CorenamespaceFreely/-!# Freely.Handler.Interpret — fold a `ℙ «Σ»` program with a handler
-/universeuvwvariable(F:Typeu→Typeu)(G:Typeu→Typeu)/-- Interpret a `ℙ «Σ» α` program into an `m α` using a `Handler «Σ» m` typeclass instance. -/definterpret{α:Typeu}{«Σ»:Typeu→Typeu}{m:Typeu→Typeu}[Monadm][«Σ»⟹ᶠm]:ℙ«Σ»α→mα:=↓ᵀHandler.handle/-- Interpret a `ℙ «Σ» α` program into `m α`, using a handler `θ : «Σ» ⟹ᶠ m` argument.
This is akin to `interpret` but accepts the handler as an argument instead of doing
an instance search.
`interpret' θ` is the free extension of a handler `θ : «Σ» ⟹ᶠ m`: the unique monad morphism
`ℙ «Σ» ⇒ m` such that `interpret' θ ∘ ↑ᵀ = θ`
⟦θ⟧ is natural in α and preserves the monad structure:
- unit: ⟦θ⟧ (ηᵀ a) = pure a (your interpret'_pure)
- multiplication/bind: ⟦θ⟧ (x >>= k) = ⟦θ⟧ x >>= (fun a => ⟦θ⟧ (k a))
-/@[noinline]definterpret'{«Σ»:Typeu→Typeu}{m:Typeu→Typeu}[Monadm](θ:«Σ»⟹ᶠm):∀{α},ℙ«Σ»α→mα:=cataFreer(F:=«Σ»)(m:=m)θ.handlenotation"⟦"θ"⟧"=>interpret'θ/-- ⟦θ⟧ preserves `pure`. -/@[simp]theoreminterpret'_pure{«Σ»:Typeu→Typeu}{m:Typeu→Typeu}[Monadm](θ:«Σ»⟹ᶠm){α}(a:α):⟦θ⟧(Freer.purea)=purea:=«Σ»:Type u→Type um:Type u→Type uinst✝:Monadmθ:«Σ»⟹ᶠmα:Type ua:α⊢ ⟦θ⟧(Freer.purea)=pureaAll goals completed! 🐙/-- ⟦θ⟧ on a single `freer`-node (via `liftFreer`). -/@[simp]theoreminterpret'_lift{«Σ»:Typeu→Typeu}{m:Typeu→Typeu}[Monadm][LawfulMonadm](θ:«Σ»⟹ᶠm){α}(s:«Σ»α):⟦θ⟧(liftFreers)=θ.handles:=«Σ»:Type u→Type um:Type u→Type uinst✝¹:Monadminst✝:LawfulMonadmθ:«Σ»⟹ᶠmα:Type us:«Σ»α⊢ ⟦θ⟧(liftFreers)=Handler.handlesAll goals completed! 🐙endFreely
11. Freely.Program.Monad.Derive.Member — generate façade senders for effect signatures🔗
This module provides a macro that inspects a continuation-passing style (CPS-style) effect constructor and produces a user-facing smart constructor that (1) keeps implicit/instance binders
implicit, (2) exposes only the explicit binders, and (3) calls send/ιᵀ.
The idea is simply to provide an ergonomic API for building programs while preserving the precise
types we want for inference. In other words, from a DSL : «Log» : String → ℙ «Σ_Log» Unit.
The deriveSend! command generates, in the current namespace, lightweight wrappers
(«c») for each constructor c of a given effect signature (inductive type).
Given a large signature (sum of effects) «Σ» : Type u → Type u and one or
more smallCPS signatures Fᵢ : Type u → Type u, the command
deriveSend! («Σ») F₁ … Fₖ
emits, for every constructor c of every Fᵢ, a definition
so you can write programs against the big signature simply as:
open MyNamespace -- the namespace where you invoked `deriveSend!`
def program : ℙ «Σ» Unit := do
«log» .info "hello"
let price ← «queryPrice» "AAPL"
«execute» "AAPL" (2 : ℚ) price
11.1. Intended setting
Each effect signature Fᵢ is an endofunctor on Type u whose
constructors are continuation style, i.e. the last parameter is a CPS
continuation (β → α), and the result is Fᵢ α.
The large signature «Σ» is built as a coproduct (e.g. via ⊕ᶠ), and
Member-instances witness that Fᵢ is a summand of «Σ».
Under these assumptions, each «c»:
injects the small effect into «Σ» using Member.inj, and
lifts it into the freer monad ℙ «Σ» using Freely.send.
11.2. What gets generated and where
For each constructor c of each Fᵢ, deriveSend! defines «c» in the
current namespace. This is deliberate: you typically call the command in
a façade namespace (e.g. namespace «Σ_finance» … end «Σ_finance»), so the resulting
«Log», «get», … are imported and opened like ordinary API functions.
The generated name is the last segment of the constructor name followed by
the superscript ᵀ (e.g. «Σ_finance».log → «Log»).
11.3. Safety and failure modes
CPS shape: if a constructor does not end in a continuation binder, the
generated …ᵀ will fail to typecheck at its definition site, pointing
precisely to the offending constructor. This is intentional: it surfaces
“non-CPS” constructors early.
Implicit binders: any implicit {…} or instance […] binders appearing
before the continuation are not made explicit; the wrapper lets Lean
infer/resolve them. Only explicit binders become lambda parameters.
Collisions: if two signatures contribute constructors with the same tail
name (e.g. two different get), generating both in the same namespace will
name-clash. When this happens we ought to consider separate namespaces, adding a macro option for a prefix/
suffix, or auto-emiting ASCII disambiguated aliases.
11.4. Auditing and inspection
After generation, we can use:
#print prefix My.Facade.Namespace to list everything created there;
#print «Log» / #check «Log» to see definitions and fully-implicit types.
11.5. Freely.Program.Monad.Derive.Member module
importFreely.Program.Monad.MemberimportLeanopenLeanMetaElabCommand/-!# Freely.Program.Monad.Derive.Member — generate façade “senders” for CPS effect signatures
-//-- Collect the outer Π-binders (names + binder-kind) from a type `ty`.
We ignore the binder *types* themselves; we only care about their names/kinds.
This walks a dependent function type `∀ n₁ : T₁, ∀ n₂ : T₂, …, body`
and returns `#[(n₁, bi₁), (n₂, bi₂), …]`. -/partialdefcollectBinders(ty:Expr)(acc:Array(Name×BinderInfo):=#[]):Array(Name×BinderInfo):=matchtywith|.forallEn_bbi=> -- one Π binder: name = `n`, body = `b`, kind = `bi`
collectBindersb(acc.push(n,bi)) -- accumulate and recurse into the body
|_=>acc -- stop when we reach a non-Π (the result type)
/-- We only turn **explicit** binders into function arguments for the wrapper.
Implicit `{…}` and instance `[…]` binders should *not* be passed explicitly. -/privatedefisExplicit:BinderInfo→Bool|.default=>true -- explicit
|_=>false -- implicit, strictImplicit, or instImplicit
/-- Generate a readable name when a constructor binder name is `_`.
We’ll call them `x1`, `x2`, … to populate the lambda telescope. -/privatedeffreshName(i:Nat)(n:Name):Name:=ifn==Name.anonymousthenName.mkSimples!"x{i.succ}"elsen/-- Extract the last (unqualified) segment of a `Name`.
Used to name the generated wrapper as `<ctorLastSeg>ᵀ`. -/privatedeflastSeg!:Name→String|.str_s=>s|.nump_=>lastSeg!p|.anonymous=>"_"/--
`deriveSend! (Σ) F₁ … Fₖ`
For every constructor `c` of each inductive `Fᵢ`, we emit *in the current namespace*:
@[inline] def cᵀ := λ (a₁ : _) … (aₙ : _) =>
Freely.send (F := Fᵢ) (R := Σ) (c a₁ … aₙ id)
Assumptions:
* The **last** constructor parameter is a CPS continuation `(β → α)`.
* Any implicit parameters (like `{α}` indices) are left implicit; Lean will generalize them.
* We make all generated binders explicit `(x : _)`; Lean will infer their types.
-/elab"deriveSend! ""("r:term")"ids:ident+:command=>do
-- Grab the current environment (definitions, inductives, etc.)
letenv←getEnv
-- `ids` is a `TSyntaxArray ident` (the `F₁ … Fₖ`). Iterate by index for robustness.
foriin[:ids.size]do
-- The i-th effect signature identifier (e.g. `Eff.EffLog`)
letidStx:TSyntax`ident:=ids[i]!letFname:=idStx.getId
-- Find inductive metadata for Fᵢ. Fail early if it's not an inductive.
letsome(.inductInfoI):=env.find?Fname|throwError"deriveSend!: {Fname} is not an inductive."
-- For each constructor of Fᵢ (e.g. `log`, `get`, `set`, …)
forctorNameinI.ctorsdo
-- Get constructor metadata
letsome(.ctorInfoci):=env.find?ctorName|throwError"deriveSend!: missing ctor info for {ctorName}"
-- 1) Read the constructor's telescope (its Π-binders) from its type.
-- Example: `log : ∀ {α}, LogLevel → String → (Unit → α) → EffLog α`
letall:=collectBindersci.typeifall.isEmptythencontinue -- nothing to do for nullary constructors
letpre:=all.pop -- drop the *last* binder = continuation
-- 2) Keep **only explicit** binders to become wrapper arguments.
-- This removes implicit `{α}` and instance `[]` binders like environments.
letexpl:Array(Name×BinderInfo):=pre.filter(fun⟨_,bi⟩=>isExplicitbi)
-- 3) Choose argument names for the explicit binders (freshen `_` to `x1`, …).
letmutargNames:ArrayName:=#[]forjin[:expl.size]dolet(n0,_):=expl[j]!argNames:=argNames.push(freshNamejn0)
-- 4) Build the **term** `c a₁ … aₙ id`
-- We quote a term `($ctorId arg1 … argN id)` using syntax quotations.
letctorId:=mkIdentctorNameletinitApp:TSyntax`term←`($ctorId)
-- fold over argument names to build `c a₁ … aₙ`
letappArgs:TSyntax`term←argNames.foldlM(init:=initApp)(funaccn=>`($acc$(mkIdentn)))
-- finally append the `id` continuation
letappWithId:TSyntax`term←`($appArgsid)
-- 5) Wrap with `Freely.send` to land in the freer monad over Σ:
-- `Freely.send (F := Fᵢ) (R := Σ) (c a₁ … aₙ id)`
letrhs:TSyntax`term←`(Freely.send(F:=$(mkIdentFname))(R:=$r)$appWithId)
-- 6) Build the lambda-telescope: `fun (a₁ : _) … (aₙ : _) => rhs`
-- We make types `_` and let Lean infer them from the ctor’s type.
letmutbody:TSyntax`term:=rhsforkinList.rangeargNames.size|>.reversedoletnm:=mkIdentargNames[k]!body←`(fun($nm:_)=>$body)
-- 7) Choose the new name in *current* namespace: `<ctorLastSeg>ᵀ`
letnewSimple:=Name.mkSimple(lastSeg!ctorName)letnewName:=mkIdentnewSimple
-- 8) Emit the definition command. No explicit type signature: Lean will infer it,
-- generalizing any implicit indices from the constructor it references.
letcmd:TSyntax`command←`(@[inline]def$newName:=($body))elabCommandcmd -- hand the generated command to the elaborator
12. Freely.Program.Monad.Freer — the free monad on an endofunctor🔗
This module implements the free(r) monadℙ F on an arbitrary mapping
F : Type u → Type u. In category-theoretic terms, given an endofunctor
F : 𝒞 ⟶ 𝒞 (here 𝒞 = Type u), the free monad ℙ F, when taken together with the
universal natural transformation ι : F ⇒ ℙ F, is the initial object in the
category of monads equipped with a morphism from F.
In other words:
Freer F α (i.e. ℙ F α) is the syntax of programs freely generated
from the effect signature F, and returning a result of type α.
The constructors are:
pure : α → ℙ F α (the monadic unit, a.k.a. ηᵀ);
freer : F β → (β → ℙ F α) → ℙ F α, which is the free “bind-shaped” step that expresses “one effect, then continue”:
an F-effect yielding a continuation β → ℙ F α.
Freely.Freer.{u,v}(F:Type u→Type v)(α:Type u):Type (max (u + 1) v)
Freely.Freer.{u,v}(F:Type u→Type v)(α:Type u):Type (max (u + 1) v)
Freer F (notation: ℙ F) — the free monad on a mapping F : Type u → Type v.
Mathematically, FreerF is the initial object among monads M equipped with
a natural transformation F⇒M. Operationally it is the syntax of programs
freely built from F-effects and pure.
pure:α→ℙFα injects a return value.
freer:Fβ→(β→ℙFα)→ℙFα is the “one effect then continue” step.
Universe: if F:Typeu→Typev, then ℙF:Typeu→Type(max(u+1)v).
Monad unit of the CPS freer monad.
Categorical reading. The unit ηᵀ:Id⇒T for T=FreerΣ, inserting a
variable as a leaf (no Σ-node). Laws (as monad morphism obligations for the
interpreter): interpret∘pure=pure, and bind(purex)k=kx.
Categorical reading. This is the “node + continuation” constructor whose
catamorphism realizes the constructor algebra α:ΣT⇒T under a CPS encoding.
Schematic type: freer:Σβ→(β→FreerΣα)→FreerΣα
It underlies the one-layer inclusion liftFreer:Σα→FreerΣα.
Universal arrow ι:F⇒ℙF. Lift an effect request into the free monad.
This is the canonical inclusion of F into the free monad it generates.
Stated somewhat more simply, programs are built up using this inductive type by either lifting pure values, or by expressing an effect along with a continuation of the program, starting from the value returned by the effect. This direct, continuation-passing encoding has two practical consequences:
Functor/Monad structure is definitionally available once F is fixed:
we provide Functor (ℙ F) and Monad (ℙ F) instances built via local
let rec functions. These let recs live under a lambda and therefore do
not trigger Lean’s structural termination checker (by design).
Interpretation (a.k.a. folding/catamorphism) is provided by
cataFreer : (F ⇒ m) → (ℙ F ⇒ m), where an algebra (handler)
alg : ∀ {β}, F β → m β induces a unique monad morphism out of ℙ F.
Given a handler (algebra) algebra:∀{β:Typeu},Fβ→mβ for some monad m,
cataFreeralgebra:ℙF⇒m is the unique monad morphism that:
sends purex to purex, and
runs one F-node at a time via algebra, then continues with the residual
program.
Termination: for a finite Freer value and a total/non-diverging
handler algebra, this fold terminates after one call to algebra per freer node.
12.1. A note on universe levels
If F : Type u → Type v, then ℙ F : Type u → Type (max (u+1) v). This is the
usual “one universe up” that comes from encoding the free monad inductively. However, we are generally interested in the case where the functor in question is an endofunctor, i.e. where F : Type u → Type u, and where F can be understood as some signature «Σ» : Type u → Type u of some domain specific language (DSL); for it is in this case that we'll be able to provide free(r) applicative FreeA «Σ» and free(r) monad ℙ «Σ» instances.
12.2. A note on termination / totality
The definitions of map, bind, and cataFreer use local let rec, so they are not checked for structural termination by Lean 4. Operationally, this means that:
map/binddo O(1) work per outer constructor and place their recursive
calls under a continuation. Evaluating map/bind on a
finite Freer value terminates immediately; no effect is run and no
recursion is forced until you interpret the term via cataFreer.
cataFreer alg reduces one F-node (one «Σ»-node) per step by calling alg, and
continuing with the residual program. Thus, for a finite Freer tree and a
total handler alg, the fold will terminate after as many steps as there are
freer nodes.
This means that so long as we use Freer as a genuine syntax tree (e.g. an inductive DSL with
finitely many constructors), these guarantees match the standard free-monad
intuition. If instead we embed general continuations that can internally
loop, cataFreer could diverge for the same reasons that our handler diverges.
12.3. A note on notation
We export the following scoped notation:
ℙ as a type constructor: ℙ F α ≡ Freer F α.
ηᵀ for pure.
↑ᵀ for liftFreer : F α → ℙ F α (the universal ι : F ⇒ ℙ F).
↓ᵀ for cataFreer (fold/interpret).
These are intended to read as the usual η/ι/catamorphism for the free monad.
12.4. Freely.Program.Monad.Freer module
namespaceFreely/-!# Freely.Program.Monad.Freer — free monad on an endofunctor.
-/universeuvw/--
Freer F (notation: ℙ F) — the free monad on a mapping F : Type u → Type v.
Mathematically, `Freer F` is the initial object among monads `M` equipped with
a natural transformation `F ⇒ M`. Operationally it is the syntax of programs
freely built from `F`-effects and `pure`.
`pure : α → ℙ F α` injects a return value.
`freer : F β → (β → ℙ F α) → ℙ F α` is the “one effect then continue” step.
Universe: if `F : Type u → Type v`, then `ℙ F : Type u → Type (max (u+1) v)`.
-/inductiveFreer(F:Typeu->Typev)(α:Typeu):Type(max(u+1)v)where/-- Monad unit of the CPS freer monad.
**Categorical reading.** The unit `ηᵀ : Id ⇒ T` for `T = Freer Σ`, inserting a
variable as a leaf (no `Σ`-node). Laws (as monad morphism obligations for the
interpreter): `interpret ∘ pure = pure`, and `bind (pure x) k = k x`.
-/|pure:α→FreerFα/-- One-step CPS constructor for the freer monad.
**Categorical reading.** This is the “node + continuation” constructor whose
catamorphism realizes the constructor algebra `α : Σ T ⇒ T` under a CPS encoding.
Schematic type: `freer : Σ β → (β → Freer Σ α) → Freer Σ α`
It underlies the one-layer inclusion `liftFreer : Σ α → Freer Σ α`.
-/|freer:∀{β:Typeu},Fβ→(β→FreerFα)→FreerFα/-- Notation for the free monad Freer. Think “`T` over `F`”. -/scopednotation"ℙ"=>Freer/-- Functor structure for the free monad `ℙ F`.
This is the unique functor extending `α ↦ α` on return values and acting
covariantly through the continuation at `freer` nodes.
Implementation note: the recursion lives under a lambda (the continuation),
so Lean does not attempt to prove structural termination. Evaluating `map` on
a finite `Freer` value does O(1) work per outer constructor and defers the
recursive call until interpretation.
-/instance{F:Typeu→Typev}:Functor(ℙF)wheremapgfreerF:=letrecrecMap{αβ:Typeu}(g:α→β):ℙFα→ℙFβ:=λprogram=>matchprogramwith|.purex=>.pure(gx)|.freereffectcontinuation=>.freereffect(λy=>recMapg(continuationy))recMapgfreerF
-- when recMap hits a pure node it will actually apply g to the value x (in that pure program that just holds a result value)
-- when recMap hits a freer node it will just leave the effect alone, but extend the continuation to evaluate the continuation,
-- and then recursively thread through the request to `map` g on the pure (result) value of the program
-- NOTE: there is no guarantee that this terminates, i.e. that the chain of continuations will ever terminate in a pure node.
-- you must be careful to ensure that this is so!
/-- Monad structure for ℙ F.
`pure` is `Freer.pure`.
`bind` sequences by threading through the continuation at `freer` nodes.
As with `map`, the recursive call sits under a continuation and does no work
until we later fold/interpret the program.
-/instance{F:Typeu→Typev}:Monad(ℙF)wherepurex:=.purexbindmcontinuation:=letrecrecBind{αβ:Typeu}(m:ℙFα)(continuation:α→ℙFβ):ℙFβ:=matchmwith|.purex=>continuationx|.freereffectcontinuation'=>.freereffect(λx'=>recBind(continuation'x')continuation)recBindmcontinuation
-- pure values are bound to continuations by extracting the value out of pure, and running the continuation on it
-- we deal with the `freer` constructor case by effectively "re-writing" the program to hold the effect expressed by the
-- program, along with an amended continuation, expressing a recursively constructed continuation that sequentially
-- unpacks and applies each `continuation'` to the input value, continued by the `continuation` we are attempting to bind
-- NOTE: there is no guarantee that this terminates. you must be careful to ensure that this is so!
-- `
/-- Universal arrow `ι : F ⇒ ℙ F`. Lift an effect request into the free monad.
This is the canonical inclusion of `F` into the free monad it generates.
-/defliftFreer{F:Typeu→Typev}{α:Typeu}(eff:Fα):ℙFα:=.freereff.pure/-- Catamorphism / interpreter for ℙ F.
Given a handler (algebra) `algebra : ∀ {β : Type u}, F β → m β` for some monad m,
`cataFreer algebra : ℙ F ⇒ m` is the unique monad morphism that:
sends `pure x` to `pure x`, and
runs one `F`-node at a time via `algebra`, then continues with the residual
program.
Termination: for a finite `Freer` value and a total/non-diverging
handler `algebra`, this fold terminates after one call to `algebra` per freer node.
-/defcataFreer{F:Typeu→Typev}{m:Typeu→Typew}[Monadm](algebra:∀{β:Typeu},Fβ→mβ):∀{α:Typeu},ℙFα→mα:=λ{α}=>letrecrecCata(t:ℙFα):mα:=matchtwith|.purex=>purex|.freereffectcontinuation=>do
-- each step runs one effect via algebra, obtains a “next state” b : β,
-- and continues with the residual program continuation b.
letresult←algebraeffectrecCata(continuationresult)recCata/-!## Notation
We provide light syntax for the common maps:
ηᵀ for Freer.pure.
↑ᵀ for liftFreer.
↓ᵀ for cataFreer.
These are scoped; enable them with open scoped Freely at use sites.
-/scopednotation"ηᵀ"=>@purescopednotation"↑ᵀ"=>@liftFreerscopednotation"↓ᵀ"=>@cataFreer@[simp]theoremcata_pure{F:Typeu→Typev}{m:Typeu→Typew}[Monadm](algebra:∀{β},Fβ→mβ){α}(x:α):cataFreer(F:=F)(m:=m)algebra(.purex)=purex:=rfl@[simp]theoremcata_freer{F:Typeu→Typev}{m:Typeu→Typew}[Monadm](algebra:∀{β},Fβ→mβ){αβ}(effect:Fβ)(continuation:β→ℙFα):cataFreer(F:=F)(m:=m)algebra(.freereffectcontinuation)=(algebraeffect>>=λb=>cataFreer(F:=F)(m:=m)algebra(continuationb)):=rfl
-- ℙ «Σ» is `Freer` in your code
@[simp]theorembind_pure_T{F:Typeu→Typev}{αβ:Typeu}(x:α)(continuation:α→ℙFβ):Bind.bind(Freer.purex)continuation=continuationx:=rfl@[simp]theorembind_freer_T{F:Typeu→Typev}{αβγ:Typeu}(effect:Fα)(k₁:α→ℙFβ)(k₂:β→ℙFγ):Bind.bind(Freer.freereffectk₁)k₂=Freer.freereffect(λa=>Bind.bind(k₁a)k₂):=rflendFreely
13. Freely.Program.Monad.Member - F is a summand of R🔗
Member F R witnesses that the endofunctor F occurs as a summand inside a
(possibly nested) sum signature R built using ⊕ᶠ. The Member F R typeclass provides
MemberFR says that F can be embedded as a summand of R.
Instance Constructor
Freely.Member.mk.{u}
Methods
inj : {α:Type u}→Fα→Rα
Canonical injection of a summand into a right-associated signature sum.
Given [MemberFR], the natural transformation inj:F⟹ᶠR is the unique
functorial inclusion determined by the right-spine decomposition of R.
Operationally, sendᵀ := (↑ᵀ) ∘ inj : F ⟹ᶠ ℙ R.
The three instances below form a reflexive–transitive closure along (the right
spine of) ⊕ᶠ:
Step-right: if F is a member of R, then F is a member of G⊕ᶠR.
Collectively, these instances should ensure that typeclass resolution automatically finds the unique injection of F into any right-associated nest G₁ ⊕ᶠ … ⊕ᶠ Gₖ that contains F.
We also provide:
send : F α → ℙ R α , which first injects into R via Member.inj, then lifts into
the free monad ℙ R (notation ιᵀ).
This is the “user-facing” operation for writing programs against a large
signature R while using constructors from a small signature F.
memberLeft uses left injection because it handles the immediate case F ⊆ (F ⊕ᶠ R)—F is already on the left, so inject with ←ᶠ.
memberRight uses right injection because it handles the inductive step: if you already know how to inject F into R, then to inject F into (G ⊕ᶠ R) you must first go to the right (past G) and then reuse the existing injection into R.
This is purely about finding F inside a nested coproduct by following a path of inl/inr tags.
Member F R gives us a structural path telling us how to embed an F α inside R α. When R is a nested sum (coproduct) of signatures, that path is literally a sequence of ←ᶠ and →ᶠ constructors.
Base case: Member F F — path is id
One-step case: Member F (F ⊕ᶠ R) — path is ←ᶠ (inl)
Inductive step: from [Member F R] derive Member F (G ⊕ᶠ R) — path is →ᶠ (inr) followed by whatever path embeds into R
Think of nested sums as a right-associated spine:
G ⊕ᶠ (H ⊕ᶠ (I ⊕ᶠ F))
The injection for F is “go right, go right, …, then left when us finally hit F”:
inj x : (G ⊕ᶠ (H ⊕ᶠ (I ⊕ᶠ F))) α
= →ᶠ (→ᶠ (→ᶠ (←ᶠ x)))
That’s exactly what memberRight composes for us step by step.
With a right associative convention, a single inductive rule (step-right) suffices to reach any position.
send is just “inject then lift”:
@[inline] def send {F R} [Member F R] {α} (fx : F α) : ℙ R α :=
↑ᵀ (Member.inj fx)
send inherits the structural path that Member builds (a stack of ←ᶠ/→ᶠ), and then turns that into a node of the free monad.
importFreely.Program.Monad.FreerimportFreely.Signature.SumnamespaceFreely/-!# Freely.Program.Monad.Member — “`F` is a summand of `R`”
-/universeu/-- `Member F R` says that `F` can be embedded as a summand of `R`. -/classMember(FR:Typeu→Typeu)where/-- Canonical injection of a summand into a right-associated signature sum.
Given `[Member F R]`, the natural transformation `inj : F ⟹ᶠ R` is the unique
functorial inclusion determined by the right-spine decomposition of `R`.
Operationally, `sendᵀ := (↑ᵀ) ∘ inj : F ⟹ᶠ ℙ R`.
-/inj{α}:Fα→Rα/-- Reflexivity: `F` is a member of itself. -/instancememberSelf(F:Typeu→Typeu):MemberFFwhereinj:=id/-- Left inclusion: `F` is a member of `F ⊕ᶠ R` via `←ᶠ`. -/instancememberLeft(FR:Typeu→Typeu):MemberF(F⊕ᶠR)whereinj:=λ{_α}x=>←ᶠx/-- Step-right: if `F` is a member of `R`, then `F` is a member of `G ⊕ᶠ R`. -/instancememberRight(FGR:Typeu→Typeu)[MemberFR]:MemberF(G⊕ᶠR)whereinj:=λ{_α}x=>→ᶠ(Member.injx)/-- Inject an `F`-effect into a larger signature `R` (using `Member`),
then lift into the free monad `ℙ R`. -/@[inline]defsend{FR:Typeu→Typeu}[MemberFR]{α:Typeu}(fx:Fα):ℙRα:=↑ᵀ(Member.injfx)scopednotation"ιᵀ"=>@Freely.sendendFreely
14. Freely.Signature.Coyoneda — deforesting maps for faster interpreters🔗
Coyoneda F α stores an F β plus a function β → α. It is isomorphic to
F, but lets us stack maps without rebuildingF nodes, which can
speed up interpreters and static analyses.
We use lift to enter, lower to exit. map composes cheaply.
/-!Freely/Coyoneda.lean -/namespaceFreelyuniverseuv/-- `Coyoneda` for an endofunctor `F`.
Store an existential `β`, an `F β`, and a post-composition `β → α`.
You may also think of this as the coend presentation `(∫^β) Hom(β, -) × F β`. -/structureCoyoneda(F:Typeu→Typev)(α:Typeu):Type(max(u+1)v)where/-- Hidden source type. -/β:Typeu/-- Underlying request. -/fi:Fβ/-- Accumulated map. -/k:β→αnamespaceCoyonedavariable{F:Typeu→Typev}{αβγ:Typeu}/-- Functor instance for `Coyoneda`: compose post-compositions.
Note that Coyoneda is a functor in α without assuming `[Functor F]`.
-/instance:Functor(CoyonedaF)wheremapfx:=⟨x.β,x.fi,f∘x.k⟩/-- `lower` folds Coyoneda back into `F`. Note that we need `[Functor F]` to push `k` through `fi`.
-/@[inline]deflower[FunctorF](x:CoyonedaFα):Fα:=Functor.mapx.kx.fi/-- Inject `F` into its Coyoneda expansion.
Lift any `F α` into `Coyoneda F α` with identity post-composition.
-/@[inline]deflift(x:Fα):CoyonedaFα:=⟨α,x,id⟩/-- Mapping inside `Coyoneda` composes with the stored post-composition. -/@[simp]theoremmap_mk(f:α→γ)(x:CoyonedaFα):(f<$>x)=⟨x.β,x.fi,f∘x.k⟩:=rfl/-- Lowering a concrete `Coyoneda.mk` is just `map` in the underlying functor. -/@[simp]theoremlower_mk[FunctorF](b:Typeu)(fi:Fb)(k:b→α):lower(Coyoneda.mkbfik)=Functor.mapkfi:=rflendCoyonedaendFreely
15. Freely.Signature.Sum — (binary) coproduct of effect signatures (endofunctors on Type u)🔗
Σ₁ ⊕ᶠ Σ₂ is a sum of DSLs, which we can think of as a sum of syntactic structures, or a sum of questions.
A program built over Σ₁ ⊕ᶠ Σ₂ may utilize Σ₁-effect orΣ₂-effect nodes. This is the additive
structure on signatures.
The idea is to:
grow vocabularies by nesting ⊕ᶠ.
handle sums by providing handlers for each side (left and right) in the sum
utilize a membership typeclass (Member F R) to witnesses that some DSL F occurs as a summand inside R, so we can write against small signatures and send effects into larger signatures, without worrying about where in the larger structure R it is that F occurs
This module defines the signature sum (binary coproduct) of endofunctors:
SumF F G : Type u → Type u with constructors
inl : F α → SumF F G α (notation ←ᶠ)
inr : G α → SumF F G α (notation →ᶠ)
Conceptually, F ⊕ᶠ G is the disjoint union of effect signatures. It carries a
canonical Functor instance whenever both F and G are functors.
Freely.SumF.{u}(FG:Type u→Type u)(α:Type u):Type u
Freely.SumF.{u}(FG:Type u→Type u)(α:Type u):Type u
SumF, written F⊕ᶠG, is the binary coproduct of endofunctors on Typeu.
This is meant to denote a sum of signaturesk the additive combinator on vocabularies of asks.
Functor structure on the sum, pointwise over the summands.
15.1. Notation & associativity
The infix combinator ⊕ᶠ is right-associative. Thus:
F ⊕ᶠ G ⊕ᶠ H ≡ F ⊕ᶠ (G ⊕ᶠ H)
(Parentheses on the right can usually be omitted.)
Injections:
←ᶠ : F α → (F ⊕ᶠ G) α
→ᶠ : G α → (F ⊕ᶠ G) α
These are the canonical coproduct maps. Together with the Freely.Program.Monad.Member class, they support implicit injection of a summand into a larger, nested signature.
namespaceFreely/-!# Freely.Signature.Sum — binary coproduct of endofunctors on `Type u`
-/universeuvwvariable(F:Typeu→Typeu)(G:Typeu→Typeu)/-- `SumF`, written `F ⊕ᶠ G`, is the binary coproduct of endofunctors on `Type u`.
This is meant to denote a sum of signaturesk the **additive** combinator on vocabularies of asks.
-/inductiveSumF(FG:Typeu→Typeu)(α:Typeu):Typeuwhere/-- Left injection of signatures: `inl : Σ₁ ⟹ᶠ (Σ₁ ⊕ᶠ Σ₂)`.
Natural in the index; categorically the coproduct inclusion. -/|inl:Fα→SumFFGα -- `←ᶠ ` injects left
/-- Right injection of signatures: `inr : Σ₂ ⟹ᶠ (Σ₁ ⊕ᶠ Σ₂)`.
Natural in the index; categorically the coproduct inclusion. -/|inr:Gα→SumFFGα -- `→ᶠ ` injects right
/-- `F ⊕ᶠ G` is the notation for binary coproduct (sum) of `F` and `G` endofunctors on `Type u` -/scopedinfixr:65" ⊕ᶠ "=>SumF/-- ←ᶠ is the notation for left injection -/scopednotation"←ᶠ"=>SumF.inl/-- →ᶠ is the notation for right injection -/scopednotation"→ᶠ"=>SumF.inr/-- Functor structure on the sum, pointwise over the summands. -/instanceinstanceFunctorSumF[FunctorF][FunctorG]:Functor(SumFFG)wheremapg
-- we map g over F ⊕ᶠ G by pattern matching on how the term was constructed,
-- and mapping over what the left or right side wraps, accordingly
|←ᶠe=>←ᶠ(Functor.mapge)|→ᶠe=>→ᶠ(Functor.mapge)endFreely
«freely» does not depend on «freely-docs». The dependencies go the other way around: «freely-docs» depends on «freely», «freely-log», and «freely-tests».
«freely» encourages us to think in terms of syntax and semantics. The structure of «freely-log» reflects this:
Signature.lean: declares the functor of operations (Σ«Log») and exports smart constructors representing the syntactic constructs of language.
Program.lean: builds terms in the free monad ℙ Σ«Log». Programs capture representations of computation still at the level of pure syntax, using the signature of the language to bind together computational expressions.
Handler.lean: provides a natural transformation Σ«Log» ⟹ᶠ m. This is the algebra that gives meaning to one step. It may depend on classes like MonadStateOf σ m and HasLoggingState σ, or be constructed from a Lens σ LoggingState. No folding occurs here though (that is interpretation - folding of a handler).
Monad.lean: selects the carrier monad (LogM m). This contains infrastructure and contexts exrpessing effects, interactions with data, etc., to be leveraged typically by handlers.
Interpret.lean: interpretation involves the catamorphism (the fold) of a program (written in such and such signature), i.e. turning ℙ Σ«Log» α into some m α, e.g. LogM α. This is the right home for run-style functions. The handler is passed/located here; the fold happens here.
This matches the algebraic picture:
«Σ» -----------------> ℙ «Σ»
| |
handler | interpret
| |
v v
target m target m
In other words, the division of labor is as follows. We:
build syntax in Program using Signature.
choose a semantics by picking a handler (usually Handler.handleLogGeneric).
execute via Interpret.run into LogM m for our base monad m.
Recap of each module:
Core : levels, state, pure formatting and emit.
Signature : CPS functor Σ«Log» and smart constructors.
Handler : generic handlers from Σ«Log» to any StateT-like monad
carrying a LoggingState.
Monad : the default carrier LogM m = StateT LoggingState m.
Interpret : run (catamorphism) from programs to effects.
Trace : combinators to pipe generic traces into this logger.
18. FreelyLog.Core - Core datatypes and pure utilities for the logging DSL🔗
This modules contains:
LogLevel: an ordered set of severities with a (total) priority function.
LoggingState: the monoidal carrier accumulating log lines plus a contextual
section stack and a threshold level.
emit: the pure update LoggingState → LoggingState that performs thresholded
emission. This is the only place that "formatting + coloring" is tied to
semantic emission.
HasLoggingState: a typeclass (a "lens-like" structure) identifying host
states σ that contain a LoggingState. This lets you interpret logging
programs inside larger application states without boilerplate.
Categorically, we have that
the signature Σ«Log» is a functor of operations (see Signature.lean).
the carrier here, LoggingState, is a parameter of the chosen Eilenberg–Moore
algebra for the monad StateT. The pure updater emit is the algebra's
effect on state when we see a logging operation.
HasLoggingState gives us a fibration-like viewpoint: any σ equipped with
a cartesian lift (get/set) can host the logging algebra (cf. lenses as
coalgebras of a store comonad).
Note: all functions here are pure. Side effects are introduced only by the handler
and interpreter layers.
importFreelyset_optionautoImplicitfalseopenFreelynamespaceFreelyLogabbrevLoggingSection:=StringabbrevLogMessage:=String/-- Minimal levels with a fixed priority order. -/inductiveLogLevel|debug|info|warn|errorderivingDecidableEq,Repr,InhabitednamespaceLogLevel@[inline]defpriority:LogLevel→Nat|.debug=>0|.info=>1|.warn=>2|.error=>3@[inline]defge(ab:LogLevel):Bool:=prioritya≥prioritybendLogLevel/-- Pure formatter: decorate a message given level and a context stack. -/@[inline]deffmt(lvl:LogLevel)(msg:String)(ctx:ListString:=[]):String:=letpath:=ifctx.isEmptythen""elses!"[{String.intercalate" ⟩⟩⟩ "ctx}] "lettag:=matchlvlwith|.debug=>"[debug] "|.info=>"[info] "|.warn=>"[warn] "|.error=>"[error] "s!"{tag}{path}{msg}"/-- Coloring -/@[noinline]defcolored(lvl:LogLevel)(s:String):String:=matchlvlwith|.debug=>termColor.debug<|s|.info=>termColor.info<|s|.warn=>termColor.warn<|s!"! {s}"|.error=>termColor.error<|s!"✗ {s}"wherecolor(lvl:LogLevel):Int:=matchlvlwith|LogLevel.debug=>6 -- cyan
|LogLevel.info=>2 -- green
|LogLevel.warn=>3 -- yellow
|LogLevel.error=>1 -- red
termColor(lvl:LogLevel)(str:String):String:=s!"\x1b[9{colorlvl}m{str}\x1b[0m"
-- structures in the monadic categories / contexts involved in handling expressions
/-- Live logging state; can be embedded inside larger states. -/structureLoggingStatewherelevel:LogLevel:=.debug -- can be changed (usually at beginning of program)
ctx:ListLoggingSection:=[] -- can be updated to hold the relevant sections stack
notes:ArrayLogMessage:=#[] -- can be updated to accumulate messages we want to log / emit
derivingRepr,Inhabited,DecidableEq/-- Push a formatted line if it meets the threshold. -/@[noinline]defemit(lvl:LogLevel)(msg:LogMessage):LoggingState→LoggingState|s=>ifLogLevel.gelvls.levelthen{swithnotes:=s.notes.push(coloredlvl<|fmtlvlmsgs.ctx)}elses/-- host states σ that *contain* a `LoggingState` -/classHasLoggingState(σ:Type)wheregetLog:σ→LoggingStatesetLog:LoggingState→σ→σ
-- identity lens when σ ≡ FreelyLog.State
instance:HasLoggingStateLoggingStatewheregetLogs:=ssetLogs_:=sendFreelyLog
Handlers turn syntax (operations in the signature Σ«Log») into semantics
(effects in a monad). Concretely, a handler is a natural transformation
θ : Σ«Log» ⟹ᶠ m
that executes one step of the CPS signature and returns the continuation's
argument.
This module provides:
Small monadic helpers modifyLog, setLevelM, pushM, popM, logAtM
that update a LoggingStateembedded in some host state σ by way of the
class HasLoggingState σ.
handleLogGeneric : a single, fully polymorphic handler that works in any
m with a MonadStateOf σ m instance and any σ that HasLoggingState.
Typeclass instances:
Freely.Handles Log m to allow interpret to discover the handler via
typeclass search whenever m has MonadStateOf σ and σ has logging.
A direct Log ⟹ᶠ m instance named handleLogInstance for convenience.
handleLogWithLensToLoggingState : a handler derived from a concrete
Lens σ LoggingState instead of the HasLoggingState class, useful if you
prefer to avoid global instances.
Note:
the handler is the chosen algebra for folding the free monad program. Each
constructor of Σ«Log» is mapped to a stateful update, followed by applying
the continuation. The genericity over σ keeps logging orthogonal to the
rest of your application state.
pushM extends the contextual "section" stack and emits an informational
line announcing entry into the section. This is a design choice; if you want
silent section boundaries, consider adding a pushQuietM.
importFreelyimportFreelyLog.CoreimportFreelyLog.MonadimportFreelyLog.ProgramimportFreelyLog.SignatureimportFreelyLog.UtilopenFreelyopenFreelyLognamespaceFreelyLogvariable{σ:Type}{m:Type→Type}@[inline]defmodifyLog[Monadm][MonadStateOfσm][HasLoggingStateσ](f:LoggingState→LoggingState):mUnit:=modify(m:=m)(σ:=σ)(fun(s:σ)=>HasLoggingState.setLog(f(HasLoggingState.getLogs))s)@[inline]defsetLevelM[Monadm][MonadStateOfσm][HasLoggingStateσ](l:LogLevel):mUnit:=modify(m:=m)(σ:=σ)(fun(s:σ)=>letls:=HasLoggingState.getLogsHasLoggingState.setLog({lswithlevel:=l})s)@[inline]defpushM[Monadm][MonadStateOfσm][HasLoggingStateσ](name:String):mUnit:=do
-- extend ctx with section name, then announce section
modify(m:=m)(σ:=σ)(fun(s:σ)=>letls:=HasLoggingState.getLogsHasLoggingState.setLog({lswithctx:=ls.ctx++[name]})s)modifyLog(m:=m)(σ:=σ)(emit.infos!"{name}")@[inline]defpopM[Monadm][MonadStateOfσm][HasLoggingStateσ]:mUnit:=modify(funs=>letls:=HasLoggingState.getLogsHasLoggingState.setLog({lswithctx:=ls.ctx.dropLast})s)@[inline]deflogAtM[Monadm][MonadStateOfσm][HasLoggingStateσ](lvl:LogLevel)(msg:String):mUnit:=modifyLog(m:=m)(σ:=σ)(emitlvlmsg)/-- One *generic* handler that works for any `σ` embedding `Log.LoggingState`. -/defhandleLogGeneric[Monadm][MonadStateOfσm][HasLoggingStateσ]:Σ«Log»⟹ᶠm:=⟨fun{_α}op=>domatchopwith|.setLevellk=>setLevelM(m:=m)(σ:=σ)l*>pure(k())|.pushnamek=>pushM(m:=m)(σ:=σ)name*>pure(k())|.popk=>popM(m:=m)(σ:=σ)*>pure(k())|.logAtlsk=>logAtM(m:=m)(σ:=σ)ls*>pure(k())⟩/-- If you want a generic Handles Log m instance, so long as m is a monad that has
state σ, and σ is the sort of data structure that has LoggingState on it... -/instance[Monadm][MonadStateOfσm][HasLoggingStateσ]:Freely.HandlesLogmwhereθ:=handleLogGeneric(m:=m)(σ:=σ)/-- If you like `interpret` to find a handler via typeclass search. -/instancehandleLogInstance[Monadm][MonadStateOfσm][HasLoggingStateσ]:Log⟹ᶠmwherehandle:=(handleLogGeneric(m:=m)(σ:=σ)).handle/-- If you like `interpret` to find a handler via a lens instead. -/defhandleLogWithLensToLoggingState[Monadm][MonadStateOfσm](lens:LensσLoggingState):Log⟹ᶠm:=⟨fun{_α}op=>doletmodifyLog(f:LoggingState→LoggingState):mUnit:=modify(fun(s:σ)=>lens.set(f(lens.gets))s)matchopwith|.setLevellk=>modify(funs=>lens.set({lens.getswithlevel:=l})s)*>pure(k())|.pushnamek=>modify(funs=>letls:=lens.gets;lens.set({lswithctx:=ls.ctx++[name]})s)*>modifyLog(emit.infos!"{name}")*>pure(k())|.popk=>modify(funs=>letls:=lens.gets;lens.set({lswithctx:=ls.ctx.dropLast})s)*>pure(k())|.logAtlmk=>modifyLog(emitlm)*>pure(k())⟩endFreelyLog
20. FreelyLog.Interpret - Catamorphic execution for programs in the free monad ℙ Σ«Log»
Given a program built from the signature Σ«Log», run folds it into the
target monad LogM m = StateT LoggingState m, using the (typeclass-discoverable)
handler from FreelyLog.Handler.
Categorically:
Σ«Log» : Type → Type is a functor (the signature).
ℙ Σ«Log» is the free monad on that functor.
A handler θ : Σ«Log» ⟹ᶠ LogM m is an algebra for the free monad.
run is the unique monad morphism induced by initiality: the catamorphism
cata θ : ℙ Σ«Log» ⇒ LogM m.
run is deliberately small: it simply applies the generic fold ↓ᵀ with the
available handler and returns both the result and the final LoggingState.
importFreelyimportFreelyLog.CoreimportFreelyLog.HandlerimportFreelyLog.MonadimportFreelyLog.SignatureopenFreelyopenFreelyLognamespaceFreelyLog/-- Run a program written in the `Log` / `Σ«Log»` Signature / DSL that takes a
`LoggingState` input to output the result `α` and `LoggingState`, in the `LogM` monad.-/@[inline]defrun{α}{m}[Monadm](p:ℙΣ«Log»α)(s₀:LoggingState:={}):m(α×LoggingState):=do(↓ᵀ(m:=LogMm)(F:=Σ«Log»)(λx=>Handler.handlex)p).runs₀
-- Recall that `↓ᵀ` (a.k.a. `cataFreer`) (recursively) folds a program `(program : ℙ F α)` as a monadic value `m α`.
-- It does this by inspecting the `program : ℙ F α`, and either returning the pure value if the program is already
-- evaluated to hold a pure return value, or recursively unpacking the contents of the `freer` constructor, which
-- holds an effect `F β`, and a continuation `β → Freer F α`:
-- ```
-- def cataFreer
-- {F : Type u → Type v}
-- {m : Type u → Type w}
-- [Monad m]
-- (algebra : ∀ {β : Type u}, F β → m β)
-- : ∀ {α : Type u}, ℙ F α → m α
-- := λ {α} =>
-- let rec recCata (t : ℙ F α) : m α :=
-- match t with
-- | .pure x => pure x
-- | .freer effect continuation => do
-- -- each step runs one effect via algebra, obtains a “next state” b : β,
-- -- and continues with the residual program continuation b.
-- let result ← algebra effect
-- recCata (continuation result)
-- recCata
-- ```
--
-- In other words, `cataFreer` takes:
-- (a) any program written in signature `F`, and
-- (b) a(n) (handler) algebra that naturally transforms `F` to `m`
-- It then "folds" the handler algebra over the monadic / applicative (and functorial) program structure,
-- unpacking effects when encountered, applying the handler algebra to the effect to obtain the result of
-- running the effect, and recursively applying the continuation to the result.
-- This is not guaranteed to terminate by itself; but if you can ensure that it does (by appropriately
-- crafting your DSL and handlers), then this fold will eventually return a monadic value `m α`
-- in the monadic context into which the handler algebra transformation takes us.
endFreelyLog
The default target monad into which the logging DSL is interpreted:
abbrev LogM (m : Type → Type) := StateT LoggingState m
This keeps the design parametric in a base monad m (e.g. Id, IO, Except ε,
…), while concentrating logging effects in a single StateT layer whose state
is precisely LoggingState.
In categorical terms: LogM is the Eilenberg–Moore carrier for our chosen
interpretation of Σ«Log»—it fixes the "algebra object" for the StateT
monad so the handler can be uniform.
The choice to only define an abbreviation (rather than a new monad) preserves
all the existing Monad, MonadStateOf, etc., instances you already have for
StateT.
importFreelyLog.CorenamespaceFreelyLog/-- Default target monad for logging -/abbrevLogM(m:Type→Type):=StateTLoggingStatemendFreelyLog
22. FreelyLog.Program
Example programs written against the logging signature Σ«Log».
Client code imports
FreelyLog.«Σ_Log» and writes programs in the free monad ℙ Σ«Log» using
the smart constructors:
«debug» «info» «warn» «error» : LogMessage → ℙ Σ«Log» Unit
The boundary between "program" and "interpretation" is kept: programs are
syntax in the free monad; they remain independent of any handler or monad
stack until we call Interpret.run.
The CPS-style signature functor Σ«Log» for logging, its Functor instance,
and smart constructors for use inside larger effect sums.
When Log is a Member of a larger signature «Σ», we export namespaced
"senders" («push», «pop», «setLevel», «logAt», and the four level-specific
abbreviations) that lift a single Log effect into ℙ «Σ».
Categorically:
Log is a functor of operation shapes.
ℙ Log is the free monad generated by those shapes.
A handler Log ⟹ᶠ m is an algebra.
Programs are initial algebras; interpretation is their unique homomorphism.
importFreelyimportFreelyLog.Core/-!### Signature -/openFreelyopenFreelyLogset_optionautoImplicitfalse/-!The `Log` / `Σ«Log»` DSL is defined principally by an inductively defined, CPS style functor. -/namespaceFreelyLog/-- CPS-style logging signature. -/inductiveLog(α:Type):Typewhere|push:String→(Unit→α)→Logα -- push a logging "section" onto the context stack
|pop:(Unit→α)→Logα -- leave a section (pop from context stack)
|setLevel:LogLevel→(Unit→α)→Logα -- set the log level threshold (below this severity, logs are not emitted)
|logAt:LogLevel→LogMessage→(Unit→α)→Logα -- log at a certain LogLevel
derivingInhabited/-- Σ«Log» is just another name for the Log signature / effect -/scopednotation"Σ«Log»"=>Log/-- Functor instance for CPS signature (map through continuation). -/instance:FunctorΣ«Log»wheremapg|.pushsectionNamek=>.pushsectionName(g∘k)|.popk=>.pop(g∘k)|.setLevellevelk=>.setLevellevel(g∘k)|.logAtlevelmessagek=>.logAtlevelmessage(g∘k)/-!The `«Σ_Log»` namespace exposes polymorphic senders for the `Log` / `Σ«Log»` DSL
(which can be imported and re-exported by other DSLs which have the `Log` / `Σ«Log»` functor as a member). -/namespace«Σ_Log»variable{«Σ»:Type→Type}[Functor«Σ»][MemberLog«Σ»]/-- Push a logging "section" onto the context stack -/@[inline]defpush(name:String):ℙ«Σ»Unit:=send(Log.pushnameid)/-- Pop a logging "section" from the context stack -/@[inline]defpop:ℙ«Σ»Unit:=send(Log.popid)/-- Set the log level threshold (below this severity, logs are not emitted) -/@[inline]defsetLevel(lvl:LogLevel):ℙ«Σ»Unit:=send(Log.setLevellvlid)/-- Log a message at a certain LogLevel -/@[inline]deflogAt(lvl:LogLevel)(msg:LogMessage):ℙ«Σ»Unit:=send(Log.logAtlvlmsgid)/-- Log a message at debug LogLevel -/@[inline]defdebug(x:LogMessage):ℙ«Σ»Unit:=logAt.debugx/-- Log a message at info LogLevel -/@[inline]definfo(x:LogMessage):ℙ«Σ»Unit:=logAt.infox/-- Log a message at warn LogLevel -/@[inline]defwarn(x:LogMessage):ℙ«Σ»Unit:=logAt.warnx/-- Log a message at error LogLevel -/@[inline]deferror(x:LogMessage):ℙ«Σ»Unit:=logAt.errorxend«Σ_Log»endFreelyLog
24. FreelyLog.Trace - Bridging generic tracing to a logging backend🔗
In this module:
pushDebug emits a single .debug line into whatever state carries a
LoggingState. It is marked @[noinline] to keep call-sites small and avoid
pessimizing hot loops that conditionally trace.
tracedToLog upgrades any base handler «Σ» ⟹ᶠ m into a traced handler
whose traces are routed to the logging state using pushDebug. This is a
combinator: it preserves the original semantics and simply adds a trace layer.
This module is intentionally thin: it has no opinions about our signature «Σ»
beyond requiring TraceF «Σ», and it keeps tracing orthogonal to both the
signature and the interpreter.
importFreelyimportFreelyLog.HandlerimportFreelyLog.SignatureopenFreelyopenFreely.DeriveTraceopenFreelyLognamespaceFreelyLog/-- Emit one debug line into whatever state carries `LoggingState`. -/@[noinline]defpushDebug{σm}[Monadm][MonadStateOfσm][HasLoggingStateσ](s:String):mUnit:=do
-- Use our log emission mechanisms; gating by current threshold happens inside `emit`.
modifyLog(m:=m)(σ:=σ)(FreelyLog.emit.debugs)pure⟨⟩/-- Turn any base handler into a traced handler whose traces go to freely-log. -/@[noinline]deftracedToLog{«Σ»mσ}[Monadm][MonadStateOfσm][HasLoggingStateσ][TraceF«Σ»](base:«Σ»⟹ᶠm):«Σ»⟹ᶠm:=tracedOf'(F:=«Σ»)(M:=m)(log:=pushDebug(σ:=σ)(m:=m))baseendFreelyLog
A minimal Lens σ τ (getter/setter pair) used by the lens-based handler
constructor. It serves as a concrete alternative to the HasLoggingState typeclass
when you prefer explicit, local wiring to global instances.
This mirrors the standard "store comonad" reading of lenses: a lens packages
the ability to focus into a component and then reassemble the whole.
namespaceFreelyLog/-- A `Lens σ τ` is a notion of a pair / product of
get and set functions on state `σ` and value `τ`. -/structureLens(στ:Type)whereget:σ→τset:τ→σ→σendFreelyLog
27. FreelyTests.Fixtures.Handlers.Tick - Algebras/handlers for the toy signature Σ«Tick»🔗
This module contains algebras/handlers for the toy signature Σ«Tick».
α_tick : Σ«Tick» ⟹ Id executes one CPS node by incrementing the
payload and feeding it to the continuation.
θ_tick : Σ«Tick» ⟹ᶠ Id is the same packaged as a Handler.
α_tick_Option, θ_tick_Option : Σ«Tick» ⟹ Option are the Option analogues
used by tests that combine handlers by zero/plus structure.
Categorically: these are Eilenberg–Moore algebras for the free monad on
Σ«Tick», targeting Id or Option. They are deliberately tiny so that
equations about ↓ᵀ (the catamorphism) become transparent in the tests.
importFreelyimportFreelyTests.Fixtures.Signature.TickopenFreelyopenscopedFreelynamespaceFreelyTests.Fixtures.Tick
-- Handlers for Id monad
/-- Algebra into `Id`: increment and feed the continuation. -/defα_tick:∀{β},SignatureTickβ→Idβ:=λ|.tickncontinuation=>continuation(n+1)/-- Same as a `Handler`. -/defθ_tick:SignatureTick⟹ᶠId:=⟨α_tick⟩
-- Handlers for Option monad
/-- Algebra into `Id`: increment and feed the continuation. -/defα_tick_Option:∀{β},SignatureTickβ→Optionβ:=λ|.tickncontinuation=>some(continuation(n+1))/-- Handle `F` by succeeding (`some`) and incrementing. -/defθ_tick_Option:Σ«Tick»⟹ᶠOption:=⟨α_tick_Option⟩endFreelyTests.Fixtures.Tick
This modules contains algebras/handlers for the toy signature Σ«Tock».
α_tock : Σ«Tock» ⟹ Id returns the payload unchanged to the continuation.
θ_tock : Σ«Tock» ⟹ᶠ Id is the packaged handler.
Used together with Tick to exercise coproducts of signatures and routing masks.
importFreelyimportFreelyTests.Fixtures.Signature.TockopenFreelyopenscopedFreelynamespaceFreelyTests.Fixtures.Tock/-- Algebra into `Id`: feed the unchanged value into the continuation. -/defα_tock:∀{β},SignatureTockβ→Idβ|_,.tockncontinuation=>continuationn/-- Same as a `Handler`. -/defθ_tock:SignatureTock⟹ᶠId:=⟨α_tock⟩endFreelyTests.Fixtures.Tock
A minimal CPS signature Σ«Tock» with one operation:
tock : Nat → (Nat → α) → Σ«Tock» α
Semantics: “pass through unchanged, then continue”. We provide the usual:
Functor instance (map through the continuation).
Smart constructor «tock» : Nat → ℙ Σ«Tock» Nat.
Paired with Σ«Tick» to form larger sum signatures for routing/masking tests.
importFreelyopenFreelyopenscopedFreelynamespaceFreelyTests.Fixtures.Tock/-- “tock” effect: return `n` unchanged via the continuation. -/inductiveSignatureTock(α:Type)where|tock:Nat→(Nat→α)→SignatureTockαderivinginstanceInhabitedforSignatureTockscopednotation"Σ«Tock»"=>SignatureTockinstance:FunctorSignatureTockwheremapg|.tocknk=>.tockn(g∘k)@[inline]deftock(n:Nat):ℙΣ«Tock»Nat:=↑ᵀ(SignatureTock.tocknid)endFreelyTests.Fixtures.Tock
31. FreelyTests.Fixtures.Tick - Σ«Tick» syntax and semantics🔗
Aggregation module: imports the Σ«Tick» signature and its canonical handlers
(Id, Option). Kept separate so tests can open a single namespace when
building programs and picking algebras.
32. FreelyTests.Fixtures.TickTock - Σ«Tick» ⊕ᶠ Σ«Tock» syntax and semantics🔗
The coproduct of toy signatures:
SignatureTickTock := Σ«Tick» ⊕ᶠ Σ«Tock»
We use deriveSend! to generate façade constructors («tick», «tock»)
that inject raw CPS nodes into the sum and lift them into the free monad.
This is the smallest setting in which we can test that interpretation over sums
is a recursive copair and that handler masking/projection behaves pointwise.
importFreelyTests.Fixtures.TickimportFreelyTests.Fixtures.TockopenFreelynamespaceFreelyTests.Fixtures.TickTockabbrevSignatureTickTock:=FreelyTests.Fixtures.Tick.SignatureTick⊕ᶠFreelyTests.Fixtures.Tock.SignatureTockscopednotation"Σ«Tick⊕Tock»"=>SignatureTickTock
-- This generates «tick» and «tock» that build nodes directly in the sum:
deriveSend!(SignatureTickTock)FreelyTests.Fixtures.Tick.SignatureTickFreelyTests.Fixtures.Tock.SignatureTock
-- #check «tick»
-- #check «tock»
endFreelyTests.Fixtures.TickTock
34. FreelyTests.Freely.Calc - equational tests as programs🔗
A tiny calculus of equalities expressed in the Σ«Log⊕Test» DSL:
Step records a named equality.
calcId / calcId_run check equalities of pure values / Id values.
calcInterp θ run checks equality after interpretation by a handler
θ : «Σ» ⟹ᶠ m followed by a post-processing run : m ⇒ ρ.
Categorically: we compare morphisms after applying a monad morphism induced by
θ (the catamorphism ↓ᵀ) and then a functor run. This is the test harness
used throughout the suite.
importFreelyimportFreelyTests.Kit.SignatureimportFreelyTests.Fixtures.TickopenFreelyopenFreelyTests.SignatureLogAndTestopenFreelyTests.Fixtures.TicknamespaceFreelyTests.Freely.CalcuniverseustructureStep(α:Typeu)wherename:Stringlhs:αrhs:α/-- Run named equalities in `Id`. -/defcalcId{α}[DecidableEqα][ToStringα](steps:List(Stepα)):ℙΣ«Log⊕Test»Unit:=doforsinstepsdoexpectEqs.names.lhss.rhs/-- Run named equalities whose payloads are `Id α`. -/defcalcId_run{α}[DecidableEqα][ToStringα](steps:List(Step(Idα))):ℙΣ«Log⊕Test»Unit:=doforsinstepsdoexpectEqs.name(Id.runs.lhs)(Id.runs.rhs)/-- Compare programs *after* interpretation by `θ : «Σ» ⟹ᶠ m` and then `run`. -/defcalcInterp{«Σ»:Typeu→Typeu}{m:Typeu→Typeu}{ρ:Typeu→Typeu}[Monadm](θ:«Σ»⟹ᶠm)(run:∀{α},mα→ρα){α:Typeu}[DecidableEq(ρα)][ToString(ρα)](steps:List(Step(ℙ«Σ»α))):ℙΣ«Log⊕Test»Unit:=doforsinstepsdoexpectEqs.name(run(⟦θ⟧s.lhs))(run(⟦θ⟧s.rhs))/-- Pretty constructor: `⟦"name"⟧ lhs ≃ rhs`. -/scopednotation"⟪"n"⟫ "lhs" ≃ "rhs=>FreelyTests.Freely.Calc.Step.mk(α:=_)nlhsrhs/-- Demo: two named links under `Id`. -/defdemoId:ℙΣ«Log⊕Test»Unit:=doleta:=(7:Nat)letb:=a+1section"calc-demo"<|docalcId[⟪"succ"⟫(a+1)≃b,⟪"commutes"⟫(1+a)≃b]/-- Demo: associativity ∘ right-identity as program equalities folded by `theta'` then `Id.run`. -/defassoc_rightId_chain:ℙΣ«Log⊕Test»Unit:=doletm:ℙSignatureTickNat:=tick1letcontinuation:Nat→ℙSignatureTickNat:=λn=>tick(n+1)calcInterp(θ:=θ_tick)(run:=Id.run)[⟪"right identity"⟫(m>>=continuation)>>=(λz=>ηᵀz)≃(m>>=continuation),⟪"associativity"⟫(m>>=continuation)≃(m>>=(λx=>continuationx>>=(λz=>ηᵀz)))]defall:ℙΣ«Log⊕Test»Unit:=dodemoIdassoc_rightId_chainendFreelyTests.Freely.Calc
lower = map on a lifted node, instantiated at Option.
Map fusion via lower: lower (map g (map f (lift x))) = map (g ∘ f) x.
Round trip: lower ∘ lift = id.
These are category-theoretic tautologies for Coyoneda, made concrete so we can
exercise the test DSL and logging layer in simple settings.
importFreelyimportFreelyLogimportFreelyTests.Kit.Signatureset_optionautoImplicitfalseopenFreelyopenFreelyTests.SignatureLogAndTestnamespaceFreelyTests.Freely.Coyoneda/-- `lower = map` on a concrete node, via `map ∘ lift`. -/deflower_eq_map:ℙΣ«Log⊕Test»Unit:=section"lower = map"<|doletfi:OptionNat:=some3letk:Nat→Nat:=(·+1)
-- Build a Coyoneda node as `map k (lift fi)`.
lett:Freely.CoyonedaOptionNat:=Functor.mapk(Freely.Coyoneda.liftfi)
-- (these are the functions under test) -- lift/map/lower live here:
-- Freely.Coyoneda.lift / .map / .lower (see core definitions)
letlhs:OptionNat:=Freely.Coyoneda.lowertletrhs:OptionNat:=Functor.mapkfiexpectEq"lower agrees with map"lhsrhs/-- Map fusion at the signature layer, witnessed after `lower`. -/deffusion:ℙΣ«Log⊕Test»Unit:=section"map fusion via lower"<|doletfi:OptionNat:=some7letf:Nat→Nat:=(·*3)letg:Nat→Nat:=(·+10)
-- left: lower (map g (map f (lift fi)))
letlhs:OptionNat:=Freely.Coyoneda.lower(Functor.mapg(Functor.mapf(Freely.Coyoneda.liftfi)))
-- right: map (g ∘ f) fi
letrhs:OptionNat:=Functor.map(g∘f)fiexpectEq"map fusion through lower"lhsrhs/-- `lower ∘ lift = id` (checked on `Option` to avoid needing functor laws). -/deflower_lift_id:ℙΣ«Log⊕Test»Unit:=section"lower ∘ lift = id (Option)"<|doleta₁:OptionNat:=noneleta₂:OptionNat:=some42letlhs₁:=Freely.Coyoneda.lower(Freely.Coyoneda.lifta₁)letlhs₂:=Freely.Coyoneda.lower(Freely.Coyoneda.lifta₂)expectEq"none round-trips"lhs₁a₁expectEq"some round-trips"lhs₂a₂/-- Suite entry point for Coyoneda tests. -/defall:ℙΣ«Log⊕Test»Unit:=dolower_eq_mapfusionlower_lift_idendFreelyTests.Freely.Coyoneda
36. FreelyTests.Freely.HandlerPlus — zero/plus and pointwise choice of handlers🔗
A toy key–value signature Σ«KV» with two Option handlers (θ_A, θ_B) is
used to validate algebraic structure on handlers:
zeroHandler annihilates every node.
⊕ᵃ (handler plus) implements left-biased, pointwise choice:
⟦θ ⊕ᵃ φ⟧ p = ⟦θ⟧ p ⊞ ⟦φ⟧ p with ⊞ = PlusM.orElse.
We test failure/success cases, left bias when both succeed, and sequential
programs to show that choice is made independently at each node.
importFreelyimportFreely.Handler.Algebra -- zeroHandler, hPlus (⊕ᵃ)
importFreely.Handler.Interpret -- interpret', ⟦θ⟧
importFreelyTests.Kit.SignatureopenFreelyopenFreelyTestsopenFreelyTests.SignatureLogAndTestnamespaceFreelyTests.Freely.HandlerPlus/-!We use a tiny CPS-style signature `Σ«KV»` with one operation `get : String → (Nat → α) → Σ«KV» α`.
Two handlers into `Option` provide disjoint coverage of keys, so `⊕ᵃ` (“try A else B”)
demonstrates left-biased choice pointwise at each node.
-/
-- signature
inductiveSignatureKV(α:Type)where|get:String→(Nat→α)→SignatureKVαscopednotation"Σ«KV»"=>SignatureKVinstance:FunctorΣ«KV»wheremapg|.getkcont=>.getk(g∘cont)@[inline]defget(key:String):ℙΣ«KV»Nat:=↑ᵀ(SignatureKV.getkeyid)
-- two complementary handlers into Option
defθ_A:Σ«KV»⟹ᶠOption:=⟨fun|.getkeyk=>ifkey="A"thenpure(k1) -- succeed with 1
else(none:Option_) -- fail
⟩defθ_B:Σ«KV»⟹ᶠOption:=⟨fun|.getkeyk=>ifkey="B"thenpure(k2) -- succeed with 2
else(none:Option_)⟩/-!## 1. Annihilation
`zeroHandler` ignores every operation and returns `ZeroM.empty` (here: `none`).
-/openFreelyTests.SignatureLogAndTestdefannihilation:ℙΣ«Log⊕Test»Unit:=doletp:ℙΣ«KV»Nat:=get"A"letlhs:OptionNat:=⟦(zeroHandlerΣ«KV»Option)⟧pletrhs:OptionNat:=noneexpectEq"zero annihilates: ⟦0⟧ = empty"lhsrhs/-!## 2. Distributivity (pointwise)
For any program `p : ℙ Σ«KV» α`,
`⟦θ_A ⊕ᵃ θ_B⟧ p = ⟦θ_A⟧ p ⊞ ⟦θ_B⟧ p` with `⊞ = PlusM.orElse`.
We test both the *fail/succeed* and the *both succeed* cases.
-/defchoice_pointwise_fail_then_succeed:ℙΣ«Log⊕Test»Unit:=doletp:ℙΣ«KV»Nat:=get"B" -- θ_A fails, θ_B succeeds
letlhs:=⟦(θ_A⊕ᵃθ_B)⟧pletrhs:=PlusM.orElse(⟦θ_A⟧p)(⟦θ_B⟧p)expectEq"interpret (θ ⊕ᵃ φ) = interpret θ ⊞ interpret φ (fail/succeed)"lhsrhsdefchoice_pointwise_both_succeed_left_biased:ℙΣ«Log⊕Test»Unit:=do
-- handlers that both succeed on any key; left bias should select θ_L's value
letθ_L:Σ«KV»⟹ᶠOption:=⟨fun|.get_k=>pure(k7)⟩letθ_R:Σ«KV»⟹ᶠOption:=⟨fun|.get_k=>pure(k9)⟩letp:ℙΣ«KV»Nat:=doget"Z"letlhs:=⟦(θ_L⊕ᵃθ_R)⟧pletrhs:=(⟦θ_L⟧p)⊞(⟦θ_R⟧p)expectEq"left bias: (θ ⊕ᵃ φ) picks θ when both succeed"lhsrhsexpectEq"value comes from left handler"lhs(some7)/-!## 3. Sequential program shows *pointwise* combination across nodes
`θ_A` covers `"A"`, `θ_B` covers `"B"`. With `⊕ᵃ`, each node falls back independently.
-/defchoice_pointwise_sequential:ℙΣ«Log⊕Test»Unit:=doletp:ℙΣ«KV»Nat:=doleta←get"A" -- picked from θ_A
letb←get"B" -- picked from θ_B (θ_A fails here)
pure(a+b) -- expect 1 + 2 = 3
letlhs:=⟦(θ_A⊕ᵃθ_B)⟧pexpectEq"pointwise across nodes (A then B)"lhs(some3)defall:ℙΣ«Log⊕Test»Unit:=dosection"handler algebra (zero/plus)"<|doannihilationchoice_pointwise_fail_then_succeedchoice_pointwise_both_succeed_left_biasedchoice_pointwise_sequentialendFreelyTests.Freely.HandlerPlus
37. FreelyTests.Freely.Handlers — coherence of handler discovery🔗
A SignatureTick ⟹ᶠ TestM handler that logs each tick, plus:
An instance Handles SignatureTick TestM so interpret can discover it.
A coherence test interpret_eq_cata showing that
the class-discovered interpreter ⟦θ⟧ and
the explicit catamorphism ↓ᵀ θ.handle
are extensionally equal (same value and same Report state).
This validates the Handles-based ergonomics around interpretation.
importFreelyimportFreelyLogimportFreelyTests.Fixtures.TickimportFreelyTests.Kit.SignatureimportFreelyTests.Kit.StateimportFreelyTests.Kit.MonadopenFreelyopenFreelyLogopenFreelyLog.«Σ_Log»openFreelyTests.Fixtures.TickopenFreelyTests.SignatureLogAndTestnamespaceFreelyTests.Freely.Handlers/-- A handler for `SignatureTick` into `TestM` that *logs* on each tick. -/defθ_tick_and_log:SignatureTick⟹ᶠTestM:=⟨fun|.ticknk=>doemit.infos!"tick {n}"pure(k(n+1))⟩/-- Give a `Handles` instance so `interpret` can use it. -/instance:HandlesSignatureTickTestMwhereθ:=θ_tick_and_log/-- `interpret` via the instance equals an explicit catamorphism with the same algebra. -/definterpret_eq_cata:ℙΣ«Log⊕Test»Unit:=dosection"handler-coherence"<|do
-- a small program that triggers the handler multiple times
letprog:ℙSignatureTickNat:=doleta←tick1letb←tick(a+2)pure(a+b)
-- (1) `interpret` using our `[Handles SignatureTick TestM]` instance
-- let m₁ : M Nat := interpret' («Σ» := SignatureTick) (m := M) prog (θ := θ_tick_and_log)
letm₁:TestMNat:=⟦θ_tick_and_log⟧prog --interpret' («Σ» := SignatureTick) (m := M) prog (θ := θ_tick_and_log)
-- (2) direct catamorphism with the same algebra
letm₂:TestMNat:=↓ᵀ(F:=SignatureTick)(m:=TestM)θ_tick_and_log.handleprogletr₀:Report:={}let(v₁,s₁):=Id.run<|m₁.runr₀let(v₂,s₂):=Id.run<|m₂.runr₀expectEq"value matches"v₁v₂expectTrue"state matches"(decide(s₁=s₂))defall:ℙΣ«Log⊕Test»Unit:=dosection"handlers"<|dointerpret_eq_cataendFreelyTests.Freely.Handlers
38. FreelyTests.Freely.Interpretation — laws for the catamorphism ↓ᵀ🔗
We check three fundamental properties (instantiated at Σ«Tick» → Id):
Unit: ↓ᵀ ∘ ηᵀ = id.
One-step: ↓ᵀ (↑ᵀ op) = θ op.
Bind homomorphism: ↓ᵀ (m >>= k) = (↓ᵀ m) >>= (fun a => ↓ᵀ (k a)).
These are the laws that make ↓ᵀ the unique monad morphism from the free monad
to the chosen target monad induced by an algebra θ.
importFreelyimportFreelyTests.Kit.SignatureimportFreelyTests.Fixtures.TickopenFreelyopenFreelyTests.SignatureLogAndTestopenFreelyTests.Fixtures.TicknamespaceFreelyTests.Freely.Interpretation/-- `↓ᵀ ∘ ηᵀ = id` -/definterpretUnit:ℙΣ«Log⊕Test»Unit:=doletx:=42letlhs:=Id.run<|↓ᵀ(F:=Σ«Tick»)(m:=Id)α_tick(ηᵀx)expectEq"↓ᵀ ∘ ηᵀ = pure"lhsx/-- `↓ᵀ` on a single op equals applying the algebra. -/definterpretOp:ℙΣ«Log⊕Test»Unit:=doletop:=.tick3idletlhs:=Id.run<|↓ᵀ(F:=Σ«Tick»)(m:=Id)α_tick(↑ᵀop)letrhs:=Id.run<|α_tickopexpectEq"↓ᵀ(↑ᵀ op) = θ op"lhsrhs/-- `↓ᵀ` respects bind (homomorphism to the target monad). -/definterpBind:ℙΣ«Log⊕Test»Unit:=doletm:ℙΣ«Tick»Nat:=tick2letcontinuation:Nat→ℙΣ«Tick»Nat:=λn=>tick(n+10)letlhs:=Id.run<|↓ᵀ(F:=Σ«Tick»)(m:=Id)α_tick(m>>=continuation)letrhs:=Id.run<|(↓ᵀ(F:=Σ«Tick»)(m:=Id)α_tickm)>>=λa=>↓ᵀ(F:=Σ«Tick»)(m:=Id)α_tick(continuationa)expectEq"↓ᵀ is monad morphism (bind)"lhsrhsdefall:ℙΣ«Log⊕Test»Unit:=dosection"interpretation (↓ᵀ laws)"<|dointerpretUnitinterpretOpinterpBindendFreelyTests.Freely.Interpretation
39. FreelyTests.Freely.MaskRouting — masking/muting and predicate routing for handlers
Two themes:
Muting a summand: muteRight lifts a handler for Σ«Tick» to
Σ«Tick⊕Tock» by killing (ZeroM) the right summand. Both interleavings of
tick and tock then evaluate to none in Option.
Routing by predicate: for a single-effect RouteF, we build masked
handlers and combine them with ⊕ᵃ to match a high-level if/else policy:
θ_route = (mask good ▹ θ_good) ⊕ᵃ (mask ¬good ▹ θ_bad) ≡ θ_if.
(The helper runOpt is a small wrapper around ↓ᵀ.)
importFreelyimportFreelyLogimportFreelyTests.Fixtures.TickimportFreelyTests.Fixtures.TockimportFreelyTests.Fixtures.TickTockimportFreelyTests.Freely.CalcimportFreelyTests.Kit.Signatureset_optionautoImplicitfalseopenFreelyopenFreelyTestsopenFreelyTests.Fixtures.TickTockopenFreelyTests.Freely.CalcopenFreelyTests.SignatureLogAndTestnamespaceFreelyTests.Freely.MaskRouting/-!### Handlers into `Option`
We purposely handle only `F` and let `G` be muted by projection.
-//-- A helper to interpret into `Option` via a given algebra. -/@[noinline]defrunOpt{α}{«Σ»:Type→Type}[Functor«Σ»](θ:«Σ»⟹ᶠOption)(p:ℙ«Σ»α):Optionα:=↓ᵀ(F:=«Σ»)(m:=Option)θ.handlep/-- Small programs over `Σ«Tick» ⊕ᶠ Σ«Tock»` that interleave nodes. -/defprog_tick_then_tock:ℙΣ«Tick⊕Tock»Nat:=doleta←tick0letb←tockapure(a+b)defprog_tock_then_tick:ℙΣ«Tick⊕Tock»Nat:=doleta←tock5letb←tickapure(a+b)/-!## Check 1: `muteRight` mutes the right summand
Using `Option` as the target (which has `ZeroM` and `PlusM`), projecting an
`Σ«Tick»`-handler to `Σ«Tick⊕Tock»` annihilates `Σ«Tock»`-nodes: both interleavings return `none`.
-/defmuteRight_check:ℙΣ«Log⊕Test»Unit:=section"masking/muting via muteRight → Option"<|doletθ_proj:Σ«Tick⊕Tock»⟹ᶠOption:=muteRightFreelyTests.Fixtures.Tick.θ_tick_Optionletr1:=runOptθ_projprog_tick_then_tockletr2:=runOptθ_projprog_tock_then_tickexpectEq"Σ«Tick» then Σ«Tock» is muted (none)"r1(none:OptionNat)expectEq"Σ«Tock» then Σ«Tick» is muted (none)"r2(none:OptionNat)
-- additionally show they’re equal *to each other* via calcInterp
calcInterp(θ:=θ_proj)(run:=(fun{α}(x:Optionα)=>x))[⟪"orders equal under muting"⟫prog_tick_then_tock≃prog_tock_then_tick]/-!## "Route-by-predicate" toy
Build two `F`-handlers and a request filter; show that
`(filter▹θ_good) ⊕ᵃ (¬filter▹θ_bad)` agrees with the intended routing policy.
-//-- A single-effect signature carrying a Boolean tag and payload. -/inductiveRouteF(α:Type):Typewhere|req:(good?:Bool)→(n:Nat)→(Nat→α)→RouteFαderivinginstanceInhabitedforRouteFinstance:FunctorRouteFwheremapg|.reqbnk=>.reqbn(g∘k)/-- "Good" and "Bad" `RouteF` handlers. We return distinct markers. -/defθ_good:RouteF⟹ᶠOption:=⟨λ|.req_nk=>some(k(n+100))⟩defθ_bad:RouteF⟹ᶠOption:=⟨λ|.req_nk=>some(k(n+200))⟩/-- Predicate on requests: route by the tag. -/defisGood:{α:Type}→RouteFα→Bool|_,.reqb__=>b/-- Mask a handler with a request predicate (mute when predicate is false). -/defθ_maskBy(p:{α:Type}→RouteFα→Bool)(θ:RouteF⟹ᶠOption):RouteF⟹ᶠOption:=⟨λop=>ifpopthenθ.handleopelse(ZeroM.empty)⟩/-- The routing algebra expressed as "algebra only" (no program rewrite). -/defθ_route:RouteF⟹ᶠOption:=(θ_maskByisGoodθ_good)⊕ᵃ(θ_maskBy(λop=>!isGoodop)θ_bad)/-- The same routing, written directly. -/defθ_if:RouteF⟹ᶠOption:=⟨λop=>ifisGoodopthenθ_good.handleopelseθ_bad.handleop⟩/-- A tiny program that hits both paths once and returns their pair. -/defprog_route:ℙRouteF(Nat×Nat):=doleta←(↑ᵀ(RouteF.reqtrue1id))letb←(↑ᵀ(RouteF.reqfalse2id))pure(a,b)/-!## Check 2: policy swap equality (routing via handler algebra)
The masked-sum algebra equals the straightforward if/else algebra on programs.
-/defpolicySwap_check:ℙΣ«Log⊕Test»Unit:=section"routing via handler algebra"<|doletr_mask:Option(Nat×Nat):=runOptθ_routeprog_routeletr_if:Option(Nat×Nat):=runOptθ_ifprog_routeexpectTrue"masked-sum ≡ if/else"(decide(r_mask=r_if))expectTrue"expected payloads"(decide(r_mask=some(101,202)))/-- Suite entry point for this file. -/defall:ℙΣ«Log⊕Test»Unit:=section"routing (masking & policy)"<|domuteRight_checkpolicySwap_checkendFreelyTests.Freely.MaskRouting
40. FreelyTests.Freely.MonadLaws — the monad laws for ℙ🔗
We validate the free monad laws on programs in Σ«Tick» by comparing programs
after interpretation through θ_tick : Σ«Tick» ⟹ᶠ Id:
Left identity: ηᵀ x >>= k ≃ k x
Right identity: m >>= ηᵀ ≃ m
Associativity: (m >>= k) >>= h ≃ m >>= (fun x => k x >>= h)
The calcInterp harness in Freely.Calc manages “interpret then compare”.
41. FreelyTests.Freely.Naturality — naturality of algebras and map fusion🔗
Three checks:
Naturality (toy) for α_tick : Σ«Tick» ⟹ Id:
α_tick (map f e) = map f (α_tick e).
Map fusion with ↓ᵀ:
↓ᵀ (map f m) = map f (↓ᵀ m).
Naturality (logging) for θ_log : Log ⟹ᶠ TestM:
θ_log (map f e) and map f (θ_log e) agree on both value and state.
These are the expected naturality squares for handlers seen as natural
transformations, made concrete for our testing monad.
importFreelyimportFreelyTests.Kit.HandlerimportFreelyTests.Fixtures.TickimportFreelyTests.Freely.InterpretationopenFreelyopenFreelyTests.SignatureLogAndTestopenFreelyTests.Fixtures.TicknamespaceFreelyTests.Freely.Naturality/-- θ naturality for the toy signature into Id. -/defθ_natural:ℙΣ«Log⊕Test»Unit:=doletf:Nat→Nat:=(·*2)lete:SignatureTickNat:=.tick3idletlhs:=Id.run<|α_tick(Functor.mapfe)letrhs:=Id.run<|f<$>α_tickeexpectEq"θ ∘ map f = map f ∘ θ (toy)"lhsrhs/-- Fusion of map with `↓ᵀ`. -/deffreer_map_fusion:ℙΣ«Log⊕Test»Unit:=doletf:Nat→Nat:=(·+10)letm:ℙSignatureTickNat:=doleta←↑ᵀ(SignatureTick.tick1id)↑ᵀ(SignatureTick.tick(a+2)id)letlhs:=Id.run<|↓ᵀ(F:=SignatureTick)(m:=Id)α_tick(Functor.mapfm)letrhs:=Id.run<|f<$>↓ᵀ(F:=SignatureTick)(m:=Id)α_tickmexpectEq"↓ᵀ (map f m) = map f (↓ᵀ m)"lhsrhs/-- Natural transformation law for the logging handler θ_log : Log ⇒ TestM. -/defθ_log_natural:ℙΣ«Log⊕Test»Unit:=dolete:FreelyLog.LogUnit:=FreelyLog.Log.setLevel.debugidletf:Unit→Unit:=idletr₀:Report:={}letlhs:=Id.run<|(FreelyTests.θ_log.handle(Functor.mapfe)).runr₀letrhs:=Id.run<|(f<$>FreelyTests.θ_log.handlee).runr₀expectTrue"θ_log naturality (value)"(decide(lhs.fst=rhs.fst))expectTrue"θ_log naturality (state)"(decide(lhs.snd=rhs.snd))defall:ℙΣ«Log⊕Test»Unit:=dosection"naturality (& map fusion)"<|doθ_naturalfreer_map_fusionθ_log_naturalendFreelyTests.Freely.Naturality
42. FreelyTests.Freely.SumHandles — auto-synthesized handlers vs explicit copairs🔗
We compare two ways to interpret Σ«Log⊕Test»:
Auto: use Handles instances and ⟦θ⟧ (class-synthesized).
Explicit: copair concrete handlers (θ_log_and_test) and fold.
expectEqRuns asserts both routes return the same (value, Report).
We also validate façade correctness: deriveSend!-generated smart
constructors (e.g. «assert») match the raw CPS nodes injected into the sum.
importFreelyimportFreelyLogimportFreelyTests.Kit.SignatureimportFreelyTests.Kit.HandlerimportFreelyTests.Kit.Interpretset_optionautoImplicitfalseopenFreelyopenFreelyLogopenFreelyTestsopenFreelyTests.SignatureLogAndTestnamespaceFreelyTests.Freely.SumHandles/-- A tiny helper to run the same program two ways and compare results. -/@[noinline]defexpectEqRuns(msg:String)(p:ℙΣ«Log⊕Test»Unit):ℙΣ«Log⊕Test»Unit:=dolet(v₁,s₁):=FreelyTests.runp -- class-synthesized handler
let(v₂,s₂):=FreelyTests.run'p -- explicit copair handler
expectEqs!"{msg} (value)"v₁v₂expectTrues!"{msg} (state)"(decide(s₁=s₂))/-- Single-node on the *left* summand (Log). -/defcopair_left:ℙΣ«Log⊕Test»Unit:=dosection"copair-left"<|do
-- one Log node
letp:ℙΣ«Log⊕Test»Unit:=doinfo"hello"expectEqRuns"copair-left (one node)"p/-- Single-node on the *right* summand (Test). -/defcopair_right:ℙΣ«Log⊕Test»Unit:=dosection"copair-right"<|do
-- one Test node
letp:ℙΣ«Log⊕Test»Unit:=doassert"ok"trueexpectEqRuns"copair-right (one node)"p/-- Two nodes (one from each side) to exercise the `↓ᵀ` recursion over sums. -/defcopair_sequence:ℙΣ«Log⊕Test»Unit:=dosection"copair-seq"<|doletp:ℙΣ«Log⊕Test»Unit:=doinfo"begin"assert"check"trueexpectEqRuns"copair (two nodes, Log then Test)"p/-- Auto-synthesis over *nested* sums also agrees extensionally with copair.
(Here our `Log ⊕ᶠ Test` is already a sum; the check is the same shape.) -/defautosynth_coincides_with_copair:ℙΣ«Log⊕Test»Unit:=dosection"autosynth"<|do
-- a slightly longer mix just to shake the tree
letp:ℙΣ«Log⊕Test»Unit:=doinfo"a"assert"b"trueinfo"c"expectEqRuns"auto-synthesized Handles ≡ copair (mixed program)"p/-- Façade correctness: the `deriveSend!`-generated `«assert»` is just
`ιᵀ ∘ Member.inj` applied to the raw CPS constructor; we validate by
showing both programs interpret to the same result. -/deffacade_correctness:ℙΣ«Log⊕Test»Unit:=dosection"facade"<|doletmsg:="facade"letok:=true
-- façade-built program
letp₁:ℙΣ«Log⊕Test»Unit:=doassertmsgok
-- raw CPS node injected then lifted
letp₂:ℙΣ«Log⊕Test»Unit:=doιᵀ(F:=Test)(R:=SignatureLogAndTest)(Member.inj(Test.assertmsgok(fun_=>())))
-- auto-synthesized handler vs explicit copair, comparing components (not pairs)
let(v₁,s₁):=runp₁{}let(v₂,s₂):=runp₂{}expectEq"façade = ιᵀ ∘ inj (value, auto-synth handler)"v₁v₂expectTrue"façade = ιᵀ ∘ inj (state, auto-synth handler)"(decide(s₁=s₂))let(v₁',s₁'):=run'p₁{}let(v₂',s₂'):=run'p₂{}expectEq"façade = ιᵀ ∘ inj (value, copair handler)"v₁'v₂'expectTrue"façade = ιᵀ ∘ inj (state, copair handler)"(decide(s₁'=s₂'))/-- Aggregate for the suite harness. -/defall:ℙΣ«Log⊕Test»Unit:=docopair_leftcopair_rightcopair_sequenceautosynth_coincides_with_copairfacade_correctnessendFreelyTests.Freely.SumHandles
43. FreelyTests.Kit.Handler — concrete handlers for the test DSL🔗
We target the test monad TestM = StateT Report Id and provide:
θ_log : Log ⟹ᶠ TestM using the generic «freely-log» handler via
HasLoggingState Report.
θ_log' : Log ⟹ᶠ TestM via an explicit lens Report ↔ LoggingState.
θ_test : Test ⟹ᶠ TestM that updates tallies and emits formatted log lines.
θ_log_and_test : (Log ⊕ᶠ Test) ⟹ᶠ TestM as a manual copair (didactic).
Handles instances so the sum handler can be derived automatically.
This module realizes the “semantics” layer for the test DSL.
importFreelyimportFreelyLogimportFreelyTests.Kit.StateimportFreelyTests.Kit.MonadimportFreelyTests.Kit.SignatureopenFreelyopenFreelyLognamespaceFreelyTests/-!## 2.4 Handlers
We *reuse* the *formatting + state* from «freely-log», and provide *two*
handlers into the *same target monad* `M`, one per summand, then copair.
-//-- Log handler specialized to `M = StateT Report`. -/defθ_log:Σ«Log»⟹ᶠTestM:=handleLogGeneric(σ:=Report)(m:=TestM)/-- You can also use a Lens to get / set the LoggingState on a Report-/defreportLogLens:LensReportLoggingState:={get:=(·.logS),set:=λlsr=>{rwithlogS:=ls}}/-- Alternatively, we can use a Lens to handle getting / setting the LoggingState on a Report,
instead of using an instance.
-/defθ_log':Log⟹ᶠTestM:=handleLogWithLensToLoggingState(σ:=Report)reportLogLens/-- Test handler: update tallies and log through the log state. -/defθ_test:Test⟹ᶠTestM:=⟨fun{_α}op=>domatchopwith|.assertmsgokk=>domodify(funr=>{rwithtotal:=r.total+1})ifokthenemit.infos!"{colored.info"✓"} {msg}"elsemodify(funr=>{rwithfailed:=r.failed+1})emit.errors!"{colored.error"✗"} {msg}"pure(k())|.xassertmsgokk=>doifokthenmodify(funr=>{rwithups:=r.ups+1})emit.errors!"{colored.error"(!) unexpected pass"} {msg}"elsemodify(funr=>{rwithxfails:=r.xfails+1})emit.infos!"{colored.warn"expected failure"} {msg}"pure(k())⟩/-- We can construct a copair to handle the sum `SignatureLogAndTest`, but this doesn't scale well.
For more elaborate sums, use the `Handles` pattern. -/defθ_log_and_test:SignatureLogAndTest⟹ᶠTestM:=⟨fun{_α}s=>matchswith|←ᶠl=>θ_log.handlel|→ᶠt=>θ_test.handlet⟩openFreelyLog/-- We can give a `Handles Test TestM` instance, and a `Handles Log TestM` instance,
(or get one for free from a `Handles Log m` instance where `m` is `MonadState σ m`, etc.)
and get a `Handles SignatureLogAndTest TestM` instance for free (see `Freely.Handler`)-/instance:HandlesTestTestMwhereθ:=θ_test/-- This instance is coming for free from `Freely.Handler` -/instance:HandlesSignatureLogAndTestTestM:=inferInstanceendFreelyTests
44. FreelyTests.Kit.Interpret — runners for the test DSL🔗
Folding programs in Σ«Log⊕Test»:
run : ℙ Σ«Log⊕Test» α → Report → (α × Report) interprets via the
class-synthesized Handles instance.
run' : ... uses the explicit copair handler.
runIO, runIO', and runIOTraced(′) print a human-readable report using the
logger output accumulated in the Report. Marked @[noinline] to keep the IR
small and avoid inlining large do-blocks in the test runner.
importFreelyimportFreelyLogimportFreelyTests.Kit.StateimportFreelyTests.Kit.HandlerimportFreelyTests.Kit.MonadimportFreelyTests.Kit.SignatureimportFreelyTests.Kit.TraceopenFreelyopenFreelyLogopenFreelyTests.SignatureLogAndTestnamespaceFreelyTests/-- A `run` function that folds programs written in the `SignatureLogAndTest` DSL
using the handler `θ` in the `Handles SignatureLogAndTest TestM` instance (which we were able to freely derive)
because the DSL is a sum / coproduct of functors, each of which has `Handles F TestM` instances.-/@[noinline]defrun{α}(program:ℙΣ«Log⊕Test»α)(r₀:Report:={}):(α×Report):=runTestM(⟦θ⟧program:TestMα)r₀
-- which is a more elegant way to write
-- Id.run <| do ((Freely.interpret' (m := TestM) («Σ» := SignatureLogAndTest) θ) program).run r₀
-- or
-- Id.run <| do (↓ᵀ (m := TestM) (F := SignatureLogAndTest) θ.handle program).run r₀
/-- Alternatively, we could have a `run'` function that performs `cataFreer` using the copairing of `Handler`s-/@[noinline]defrun'{α}(program:ℙΣ«Log⊕Test»α)(r₀:Report:={}):(α×Report):=runTestM(⟦θ_log_and_test⟧program:TestMα)r₀@[noinline]defrunIO(program:ℙΣ«Log⊕Test»α)(r₀:Report:={}):IOUnit:=dolet(_result,report):=runprogramr₀IO.println(testsOutputreport)@[noinline]defrunIO'(program:ℙΣ«Log⊕Test»α)(r₀:Report:={}):IOUnit:=dolet(_result,report):=run'programr₀IO.println(testsOutputreport)@[noinline]defrunIOTraced(program:ℙΣ«Log⊕Test»α)(r₀:Report:={}):IOUnit:=dolet(_result,report):=runprogramr₀IO.println(testsOutputreport)@[noinline]defrunIOTraced'(program:ℙΣ«Log⊕Test»α)(r₀:Report:={}):IOUnit:=dolet(_result,report):=run'programr₀IO.println(testsOutputreport)endFreelyTests
withLog, emit convenience functions to update/append through «freely-log».
emit delegates formatting/thresholding to Freely.Log.emit, keeping logging
policy centralized in «freely-log».
importFreelyLogimportFreelyTests.Kit.Stateset_optionautoImplicitfalseopenFreelyLognamespaceFreelyTests/-- All handlers will target this state monad. -/abbrevTestM:=StateTReportId@[noinline]defrunTestM{α}(m:TestMα)(r₀:Report:={}):(α×Report):=Id.run<|do(m:TestMα).runr₀instance:MonadTestM:=inferInstance@[inline]defwithLog(f:LoggingState→LoggingState):TestMUnit:=modify(funr=>{rwithlogS:=fr.logS})/-- Convenience: append a line via the freely-log formatter & threshold. -/@[inline]defemit(lvl:LogLevel)(msg:String):TestMUnit:=withLog(FreelyLog.emitlvlmsg)endFreelyTests
46. FreelyTests.Kit.Program — demo suite as a program🔗
demoSuite : ℙ Σ«Log⊕Test» Unit is a small integration program that:
manipulates logging sections and levels,
performs assertions and expected-failure checks,
produces a readable transcript when interpreted through Kit.Interpret.
This serves both as an example and as a quick “smoke test” for the harness.
«expectTrue», «expectEq» build test nodes with messages.
Re-export of logging façade («push», «info», …) for one-stop imports.
«section» is marked @[noinline] (see comment in file) to avoid optimizer
fragility with small do-blocks on Lean 4.23.0-rc2.
This is the syntax layer for the test harness.
importFreelyimportFreelyLogset_optionautoImplicitfalseopenFreelyopenFreelyLognamespaceFreelyTests/-- Minimal test signature: assertions + expected-failure assertions. -/inductiveTest(α:Type):Typewhere|assert:(msg:String)→(ok:Bool)→(Unit→α)→Testα|xassert:(msg:String)→(ok:Bool)→(Unit→α)→TestαderivingInhabitedscopednotation"Σ«Test»"=>Testinstance:FunctorTestwheremapg|.assertmokcontinuation=>.assertmok(g∘continuation)|.xassertmokcontinuation=>.xassertmok(g∘continuation)
-- mapping `g` over this language just involves threading `g` through so that it
-- it is composed with the continuation, and applies after the continuation is applied,
-- (which continuation chain we hope eventually terminates in a pure node containing a pure value
-- to which `g` may eventually be applied...)
/-- The tests DSL *includes* logging as a summand. -/abbrevSignatureLogAndTest:=Log⊕ᶠTestnamespaceSignatureLogAndTestderiveSend!(SignatureLogAndTest)FreelyTests.Test
-- this gives us:
-- «assert»
-- «xassert»
scopednotation"Σ«Log⊕Test»"=>SignatureLogAndTest
-- we add a few more convenience expressions to the language
@[noinline]defexpectTrue(msg:String)(b:Bool):ℙΣ«Log⊕Test»Unit:=asserts!"{msg}"b@[noinline]defexpectEq{α}[DecidableEqα][ToStringα](msg:String)(xy:α):ℙΣ«Log⊕Test»Unit:=asserts!"{msg} (got {x}, expected {y})"(decide(x=y))
-- and we reexport what we want from the Log language
exportFreelyLog.«Σ_Log»(
-- Push a logging "section" onto the context stack
push
-- Pop a logging "section" from the context stack
pop
-- Set the log level threshold (below this severity, logs are not emitted)
setLevel
-- Log a message at a certain LogLevel
logAt
-- Log a message at debug LogLevel
debug
-- Log a message at info LogLevel
info
-- Log a message at warn LogLevel
warn
-- Log a message at error LogLevel
error)
-- On 4.23.0-rc2 there’s a known fragility around inlining “small” monadic do blocks with string literals; it sometimes corrupts IR and crashes later with 139.
-- So we use @[nonoinline] for “control” helpers (logging, tracing, resource management, or wrappers around effects) and for small do-blocks we call many times.
-- This avoids IR duplication and brittle optimizer interactions.
@[noinline]defsection{α:Type}(label:String)(k:ℙΣ«Log⊕Test»α):ℙΣ«Log⊕Test»α:=dopushlabelleta←kpoppureaendSignatureLogAndTestendFreelyTests
48. FreelyTests.Kit.State — run report and logging integration🔗
testsOutput : Report → String pretty-prints the accumulated log followed by a
summary line. Keeping logs inside Report makes the logger pluggable and
composable with other effects.
importFreelyLogset_optionautoImplicitfalseopenFreelyLognamespaceFreelyTests/-- Test run summary + accumulated log state. -/structureReportwheretotal:Nat:=0failed:Nat:=0xfails:Nat:=0ups:Nat:=0logS:LoggingState:={}derivingRepr,Inhabited,DecidableEqinstance:HasLoggingStateReportwheregetLogr:=r.logSsetLoglsr:={rwithlogS:=ls}@[inline]deftestsOutput(r:Report):String:=letresults:=s!"tests: {r.total}, failed: {r.failed}, xfail: {r.xfails}, upass: {r.ups}"(testLogsr.logS)++"\n"++resultswheretestLogs(s:LoggingState):String:=String.intercalate"\n"s.notes.toListendFreelyTests
49. FreelyTests.Kit.Trace — traced handlers, routed to the logger🔗
We derive TraceF instances for Log and Test and then:
θ_log_traced : Log ⟹ᶠ TestM
θ_test_traced : Test ⟹ᶠ TestM
by applying Freely.Log.tracedToLog, which wraps a base handler so that its
trace events are emitted into the LoggingState. The copair
θ_log_and_test_traced handles the sum when both components are traceable.
git clone https://github.com/wadkisson/lean4code
cd lean4code
cd extensions
npm i -D @vscode/codicons@0.0.39 @vscode-elements/elements
cd lean4
npm run build
cd ..
cd vscode/
npm ci
npm run build-extensions
npm install
If you're on MacOS you may need to do something like:
NODE_OPTIONS="--max-old-space-size=8192" npx gulp vscode-darwin-arm64
xattr -rd com.apple.quarantine <path to Lean4Code.app>
Finally, you might want to add an alias to your .zshrc (or .bashrc, etc.):
Copyright (c) 2025 Abraham Howland and «freely» contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
51. Notice
«freely» — NOTICE
Copyright (c) 2025 Abraham Howland and «freely» contributors
Licensed under the Apache License, Version 2.0 (the "License").
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
This NOTICE file is provided for informational purposes only and does not modify
the License. If this distribution includes third‑party components, their licenses
and notices are provided in the corresponding source files or accompanying
documents.
52. Citing «freely»
cff-version: 1.2.0
title: freely
message: If you use this software, please cite it as below.
authors:
- family-names: Howland
given-names: Abraham
- name: "freely contributors"
version: 0.1.0
license: Apache-2.0
․ Acknowledgments
Some code in this repository was drafted with the assistance of large language model tools. All contributed code was reviewed, selected, and modified by human authors.