«freely»

«freely»: a Lean 4 library enabling free(r) syntax, modular semantics

Abraham Howland

Table of Contents

  1. ⁙ «freely»
  2. 1. Introducing «freely»
    1. 1.1. Repository
    2. 1.2. Language
    3. 1.3. Dependencies
    4. 1.4. Purpose of «freely»
    5. 1.5. «freely», at a glance (where to start)
    6. 1.6. Motivation behind «freely»
    7. 1.7. Separating Syntax from Semantics
    8. 1.8. How is this "free(r)" expression?
    9. 1.9. Disclaimer
  3. 2. Notation in «freely»
  4. 3. Prior Art
    1. 3.1. Prior art around Free(r)
      1. 3.1.1. Papers
      2. 3.1.2. Haskell libraries
      3. 3.1.3. Library-by-library comparison (very simplified)
    2. 3.2. Prior art around Functor Combinators
  5. ⟩ «freely», categorically
    1. Dedication
  6. 4. Universes, and the ambient category
    1. 4.1. Universe levels and smallness
      1. 4.1.1. Practical rule
    2. 4.2. Type u has a cartesian-closed core with finite coproducts
    3. 4.3. Endofunctors, free monads, and handlers at universe level u
    4. 4.4. Why not bare Type?
    5. 4.5. «freely» design contract
  7. 5. Introduction to the categorical viewpoint
    1. 5.1. An overview of the categorical picture pursued in «freely»
  8. 6. Deepening the categorical picture
    1. 6.1. Ambient setting
    2. 6.2. The free monad on a signature
    3. 6.3. Target semantics: (M, θ)
    4. 6.4. Interpreter, and the universal commuting law
      1. 6.4.1. Commuting square
    5. 6.5. Category of «Σ»-interpreters and initiality
    6. 6.6. Modularity: sums, membership, copairing
    7. 6.7. Minimal hypotheses (what is strictly required)
    8. 6.8. Two categorical viewpoints: Kleisli and Eilenberg–Moore
      1. 6.8.1. Kleisli viewpoint
      2. 6.8.2. Eilenberg–Moore viewpoint
  9. ⁘ Freely
  10. 7. Freely.Algebra.ZeroPlus — additive structure for targets & handlers
  11. 8. Freely.Handler.Algebra
  12. 9. Freely.Handler.Core — natural transformations as effect handlers
  13. 10. Freely.Handler.Interpret — folding free programs with a handler
  14. 11. Freely.Program.Monad.Derive.Member — generate façade senders for effect signatures
    1. 11.1. Intended setting
    2. 11.2. What gets generated and where
    3. 11.3. Safety and failure modes
    4. 11.4. Auditing and inspection
    5. 11.5. Freely.Program.Monad.Derive.Member module
  15. 12. Freely.Program.Monad.Freer — the free monad on an endofunctor
    1. 12.1. A note on universe levels
    2. 12.2. A note on termination / totality
    3. 12.3. A note on notation
    4. 12.4. Freely.Program.Monad.Freer module
  16. 13. Freely.Program.Monad.Member - F is a summand of R
  17. 14. Freely.Signature.Coyoneda — deforesting maps for faster interpreters
  18. 15. Freely.Signature.Sum — (binary) coproduct of effect signatures (endofunctors on Type u)
    1. 15.1. Notation & associativity
  19. ⁙ «freely-docs»
  20. 16. Purpose of «freely-docs»
  21. ⁙ «freely-log»
  22. 17. Structure of «freely-log»
  23. ⁘ FreelyLog
  24. 18. FreelyLog.Core - Core datatypes and pure utilities for the logging DSL
  25. 19. FreelyLog.Handler
  26. 20. FreelyLog.Interpret - Catamorphic execution for programs in the free monad ℙ Σ«Log»
  27. 21. FreelyLog.Monad
  28. 22. FreelyLog.Program
  29. 23. FreelyLog.Signature
  30. 24. FreelyLog.Trace - Bridging generic tracing to a logging backend
  31. 25. FreelyLog.Util
  32. ⁙ «freely-tests»
  33. ⁘ FreelyTests
  34. 26. FreelyTests.Suites.Freely
    1. 26.1. Results
  35. 27. FreelyTests.Fixtures.Handlers.Tick - Algebras/handlers for the toy signature Σ«Tick»
  36. 28. FreelyTests.Fixtures.Handlers.Tock
  37. 29. FreelyTests.Fixtures.Signature.Tick - signature Σ«Tick»
  38. 30. FreelyTests.Fixtures.Signature.Tock - signature Σ«Tock»
  39. 31. FreelyTests.Fixtures.Tick - Σ«Tick» syntax and semantics
  40. 32. FreelyTests.Fixtures.TickTock - Σ«Tick» ⊕ᶠ Σ«Tock» syntax and semantics
  41. 33. FreelyTests.Fixtures.Tock
    1. 33.1. FreelyTests.Fixtures.Tock
  42. 34. FreelyTests.Freely.Calc - equational tests as programs
  43. 35. FreelyTests.Freely.Coyoneda
  44. 36. FreelyTests.Freely.HandlerPlus — zero/plus and pointwise choice of handlers
  45. 37. FreelyTests.Freely.Handlers — coherence of handler discovery
  46. 38. FreelyTests.Freely.Interpretation — laws for the catamorphism ↓ᵀ
  47. 39. FreelyTests.Freely.MaskRouting — masking/muting and predicate routing for handlers
  48. 40. FreelyTests.Freely.MonadLaws — the monad laws for
  49. 41. FreelyTests.Freely.Naturality — naturality of algebras and map fusion
  50. 42. FreelyTests.Freely.SumHandles — auto-synthesized handlers vs explicit copairs
  51. 43. FreelyTests.Kit.Handler — concrete handlers for the test DSL
  52. 44. FreelyTests.Kit.Interpret — runners for the test DSL
  53. 45. FreelyTests.Kit.Monad — the test carrier
  54. 46. FreelyTests.Kit.Program — demo suite as a program
  55. 47. FreelyTests.Kit.Signature - the test DSL and its sum with logging
  56. 48. FreelyTests.Kit.State — run report and logging integration
  57. 49. FreelyTests.Kit.Trace — traced handlers, routed to the logger
  58. ․ Local Development
    1. ⁚ Repository Structure
    2. Local build scripts
    3. Tooling you might need
      1. python3
      2. lean4code IDE (clone of vscode w/ lean specific bells and whistles)
  59. ․ License, citation, and use of «freely»
  60. 50. License (Apache 2.0)
  61. 51. Notice
  62. 52. Citing «freely»
  63. ․ Acknowledgments
  64. 53. ․ Resources
    1. 53.1. ⁚ Lean 4 resources
    2. 53.2. ⁚ Category Theory resources (unrelated to free(r) monads)
      1. 53.2.1. ⁖ Sites
      2. 53.2.2. ⁖ Category Theory Books
      3. 53.2.3. ⁖ String Diagrams Books (Category Theory meets Computation)
      4. 53.2.4. ⁖ Topos Theory / Sheaf Theory Books
  65. ․ Author (Abraham Howland)
    1. ⁚ Résumé
    2. ⁚ Contact

⁙ «freely»🔗

This manual covers version 0.1.0 of «freely», pinned to the lean-toolchain: leanprover/lean4:v4.23.0-rc2

1. Introducing «freely»

What is meant by "free(r)" expression? How can a language, or a library, enable free(r) forms of expression, free(r) ways of conveying meaning?

1.1. Repository

The code for the «freely» (Lean 4) library is hosted on GitHub at:

For information on license, citation, and use of «freely», take a look at this section.

1.2. Language

«freely» is written in Lean 4, a functional language built for formal verification.

Those new to Lean should carve out some time to take a look at our Lean 4 resources.

1.3. Dependencies

«freely» has minimal dependencies. Specifically, «freely» does not depend on mathlib.

Additionally, we should note that «freely» does not depend upon «freely-docs», «freely-log», or «freely-tests». Instead, those libraries depend upon «freely». This means that «freely» does not depend upon verso (only subverso).

1.4. Purpose of «freely»

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

  • handlers should be natural transformations (Handler «Σ» m, i.e «Σ» ⟹ᶠ m) mapping signature functors («Σ») to endofunctors / monads m : Type u → Type u

  • 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:

If you want to see basic examples of using «freely», you might take a look at:

«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.

1.7. Separating Syntax from Semantics

When using «freely», you will:

  • 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)

unit

monad

ηᵀ

ηᵀ : ∀ {α}, α → ℙ «Σ» α (aka pure)

lift/inclusion ι: «Σ» ⇒ ℙ «Σ»

monad

↑ᵀ

↑ᵀ : «Σ» ⟹ᶠ ℙ «Σ» (components ∀ {α}, «Σ» α → ℙ «Σ» α)

fold / catamorphism

monad

↓ᵀ

↓ᵀ : (θ : «Σ» ⟹ᶠ M) → (∀ {α}, ℙ «Σ» α → M α); equivalently ↓ᵀ θ : MonadMorph (ℙ «Σ») M

send into ℙ R

monad

sendᵀ

sendᵀ : [Member «Σ_Foo» R] → (∀ {α}, «Σ_Foo» α → ℙ «Σ_Baz» α) (usually sendᵀ = ↑ᵀ ∘ inj)

façade ops (type ℙ «Σ»)

monad

«log», «get», …

For each op o: «op» : (args …) → ℙ «Σ» (result) (domain-specific smart constructors over ℙ «Σ»)

interpreter (unique)

monad morphism / semantics

⟦-⟧_θ : ℙ ⇒ M

Componentwise ⟦-⟧_θ : ∀ {α}, ℙ «Σ» α → M α; packaged ⟦-⟧_θ : MonadMorph (ℙ «Σ») M (interpret θ)

constructor algebra (free)

«Σ»-algebra (initial)

α : «Σ» ℙ ⇒ ℙ

As a nat. trans.: α : «Σ» ∘ ℙ «Σ» ⟹ᶠ ℙ «Σ»; components α_α : «Σ» (ℙ «Σ» α) → ℙ «Σ» α

E–M algebra (consumer of M)

Eilenberg–Moore algebra

a : M A → A

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.

3.1. Prior art around Free(r)

3.1.1. Papers

3.1.2. Haskell libraries

  • extensible-effects ("extensible effects" library, leveraging open unions)

  • freer-simple (lightweight "freer" implementation, with Template-Haskell helpers for smart constructors)

  • fused-effects ("algebraic carriers" - not a freer encoding)

  • polysemy ("final tagless with effects" - not a freer encoding)

3.1.3. Library-by-library comparison (very simplified)

Dimension

Freely (Lean)

extensible-effects

freer-simple

fused-effects / polysemy

Signature

«Σ» : Type u → Type u (functor; CPS-encoding)

Effect GADTs; type-level "row" of effects (open union)

Effect GADTs; type-level "row" with Member eff r

Not freer; algebraic carriers (fused-effects) / final-tagless (polysemy)

Sum / coproduct of signatures

⊕ᶠ with Member F R

Open unions / rows

Open unions / rows

Stacks of carriers / constraints (not a functor sum)

Smart constructors

deriveSend! generates façade ops in ℙ «Σ»

Hand written or helper functions

Template Haskell makeEffect to derive send-style functions

Template Haskell makeSem (polysemy) / carrier-specific helpers (fused-effects)

Handler

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.

⟩ «freely», categorically🔗

Dedication

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:

universe u variable («Σ» : Type u Type u) [Functor «Σ»] variable (M : Type u Type u) [Monad M]

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 «Σ» : 𝒞 ⟶ 𝒞.

«freely» pursues the viewpoint that we can have:

  • a Free monad ℙ «Σ» (our Freer «Σ») with unit ηᵀ : Id ⇒ ℙ and the universal transformation ↑ᵀ : «Σ» ⟹ᶠ ℙ «Σ»

    Our Freer is the continuation-passing style (CPS) free-monad encoding which is operationally the "freer monad" of Kiselyov & Ishii

    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.

    🔗inductive type
    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, 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).

    Constructors

    pure.{u, v} {F : Type u  Type v} {α : Type u} : α   F α

    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.

    freer.{u, v} {F : Type u  Type v} {α β : Type u} :
      F β  (β   F α)   F α

    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 Σ α.

    🔗def
    Freely.liftFreer.{u, v} {F : Type u Type v} {α : Type u} (eff : F α) : F α
    Freely.liftFreer.{u, v} {F : Type u Type v} {α : Type u} (eff : F α) : F α

    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.

  • handlers which simply wrap natural transformations

    The class

    🔗type class
    Freely.Handler.{u} («Σ» m : Type u Type u) : Type (u + 1)
    Freely.Handler.{u} («Σ» m : Type u Type u) : Type (u + 1)

    A natural transformation «Σ» ⟹ᶠ m (component-wise «Σ» β m β).

    Instance Constructor

    Freely.Handler.mk.{u}

    Methods

    handle : {β : Type u}  «Σ» β  m β

    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.

    🔗def
    Freely.cataFreer.{u, v, w} {F : Type u Type v} {m : Type u Type w} [Monad m] (algebra : {β : Type u} F β m β) {α : Type u} : F α m α
    Freely.cataFreer.{u, v, w} {F : Type u Type v} {m : Type u Type w} [Monad m] (algebra : {β : Type u} F β m β) {α : Type u} : F α m α

    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.

    🔗def
    Freely.interpret.{u} {α : Type u} {«Σ» m : Type u Type u} [Monad m] [«Σ» ⟹ᶠ m] : «Σ» α m α
    Freely.interpret.{u} {α : Type u} {«Σ» m : Type u Type u} [Monad m] [«Σ» ⟹ᶠ m] : «Σ» α m α

    Interpret a «Σ» α program into an m α using a Handler «Σ» m typeclass instance.

    🔗def
    Freely.interpret'.{u} {«Σ» m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} : «Σ» α m α
    Freely.interpret'.{u} {«Σ» m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} : «Σ» α 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))

  • a co-product of signatures

    We implement «Σ₁» ⊕ᶠ «Σ₂» as SumF, in a pointwise functorial manner.

    🔗inductive type
    Freely.SumF.{u} (F G : Type u Type u) (α : Type u) : Type u
    Freely.SumF.{u} (F G : Type u Type u) (α : Type u) : Type u

    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.

    Constructors

    inl.{u} {F G : Type u  Type u} {α : Type u} :
      F α  (F ⊕ᶠ G) α

    Left injection of signatures: inl : Σ₁ ⟹ᶠ (Σ₁ ⊕ᶠ Σ₂). Natural in the index; categorically the coproduct inclusion.

    inr.{u} {F G : Type u  Type u} {α : Type u} :
      G α  (F ⊕ᶠ G) α

    Right injection of signatures: inr : Σ₂ ⟹ᶠ (Σ₁ ⊕ᶠ Σ₂). Natural in the index; categorically the coproduct inclusion.

    Handlers for such sums are simply co-pairings of handlers (θ₁, θ₂) : «Σ₁» ⇒ M × «Σ₂» ⇒ M giving θ : «Σ₁» ⊕ᶠ «Σ₂» ⇒ M, as one would expect.

    🔗def
    Freely.copair.{u} {F G m : Type u Type u} (θF : F ⟹ᶠ m) (θG : G ⟹ᶠ m) : (F ⊕ᶠ G) ⟹ᶠ m
    Freely.copair.{u} {F G m : Type u Type u} (θF : F ⟹ᶠ m) (θG : G ⟹ᶠ m) : (F ⊕ᶠ G) ⟹ᶠ m

    Copairing of handlers. Given θ₁ : «Σ₁» ⟹ᶠ M and θ₂ : «Σ₂» ⟹ᶠ M, produce [θ₁, θ₂] : («Σ₁» ⊕ᶠ «Σ₂») ⟹ᶠ M with the defining equations [θ₁, θ₂] inl = θ₁ and [θ₁, θ₂] inr = θ₂.

    🔗def
    Freely.handleSum.{u} {F G m : Type u Type u} (θF : F ⟹ᶠ m) (θG : G ⟹ᶠ m) : (F ⊕ᶠ G) ⟹ᶠ m
    Freely.handleSum.{u} {F G m : Type u Type u} (θF : F ⟹ᶠ m) (θG : G ⟹ᶠ m) : (F ⊕ᶠ G) ⟹ᶠ m

    Handler for a sum of signatures, implemented as copairing of component handlers.

    🔗type class
    Freely.Handles («Σ» m : Type Type) : Type 1
    Freely.Handles («Σ» m : Type Type) : Type 1

    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.

    Instance Constructor

    Freely.Handles.mk

    Methods

    θ : «Σ» ⟹ᶠ m

    θ : «Σ» ⟹ᶠ M (a natural transformation from the signature to the semantic monad).

    🔗def
    Freely.handlesSum {F G m : Type Type} [Handles F m] [Handles G m] : Handles (F ⊕ᶠ G) m
    Freely.handlesSum {F G m : Type Type} [Handles F m] [Handles G m] : Handles (F ⊕ᶠ G) m

    Recursive instance for sums (co-pairing).

    These constructions exhibit the categorical universal property of initiality in the category of endofunctors over 𝒞, i.e. End(𝒞). (Note: This is the data types à la carte pattern.)

  • membership

    Our

    Member F R  ⊣  inj : F ⇒ R
    

    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.

    🔗type class
    Freely.Member.{u} (F R : Type u Type u) : Type (u + 1)
    Freely.Member.{u} (F R : Type u Type u) : Type (u + 1)

    Member F R 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 [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.

  • smart constructors

    Our deriveSend! («Σ») F₁ … Fₖ generates façade functions

    «c» : … → ℙ «Σ» _
    

    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 signatures: (⊕ᶠ, ⊗ᶠ; 0ᶠ, 𝟙ᶠ) (syntax)

  • 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.

The catamorphism/fold is cataFreer:

🔗def
Freely.cataFreer.{u, v, w} {F : Type u Type v} {m : Type u Type w} [Monad m] (algebra : {β : Type u} F β m β) {α : Type u} : F α m α
Freely.cataFreer.{u, v, w} {F : Type u Type v} {m : Type u Type w} [Monad m] (algebra : {β : Type u} F β m β) {α : Type u} : F α m α

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.

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

⟦-⟧_θ ∘ α = θ ∘ «Σ»(⟦-⟧_θ)
🔗def
Freely.interpret.{u} {α : Type u} {«Σ» m : Type u Type u} [Monad m] [«Σ» ⟹ᶠ m] : «Σ» α m α
Freely.interpret.{u} {α : Type u} {«Σ» m : Type u Type u} [Monad m] [«Σ» ⟹ᶠ m] : «Σ» α m α

Interpret a «Σ» α program into an m α using a Handler «Σ» m typeclass instance.

🔗def
Freely.interpret'.{u} {«Σ» m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} : «Σ» α m α
Freely.interpret'.{u} {«Σ» m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} : «Σ» α 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))

  • Laws

    α = join ∘ liftFreer

    interpret θ ∘ liftFreer = θ.handle
    interpret θ preserves pure / bind
    
    🔗theorem
    Freely.interpret'_pure.{u} {«Σ» m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} (a : α) : θ (Freer.pure a) = pure a
    Freely.interpret'_pure.{u} {«Σ» m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} (a : α) : θ (Freer.pure a) = pure a

    ⟦θ⟧ preserves pure.

    🔗theorem
    Freely.interpret'_lift.{u} {«Σ» m : Type u Type u} [Monad m] [LawfulMonad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} (s : «Σ» α) : θ (liftFreer s) = Handler.handle s
    Freely.interpret'_lift.{u} {«Σ» m : Type u Type u} [Monad m] [LawfulMonad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} (s : «Σ» α) : θ (liftFreer s) = Handler.handle s

    ⟦θ⟧ on a single freer-node (via liftFreer).

6.4.1. Commuting square

«Σ» 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)

  1. 𝒞 has finite products/coproducts (to form polynomial «Σ»).

  2. The free monad ℙ «Σ» exists.

  3. 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:

  1. Canonical meaning: fixing θ yields a unique ⟦-⟧_θ.

  2. Functorial transport: if φ : M ⇒ N preserves handlers, then φ ∘ ⟦-⟧_θ = ⟦-⟧_{θ’}.

  3. Compositionality by construction: evaluation is an algebra homomorphism; substitution is μ.

  4. Modularity and extension: extending «Σ» via sums extends programs and handlers modularly.

  5. 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.

⁘ Freely🔗

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

    🔗type class
    Freely.ZeroM.{u} (m : Type u Type u) : Type (u + 1)
    Freely.ZeroM.{u} (m : Type u Type u) : Type (u + 1)

    A codomain with a distinguished "no result". Typical instances: Option.

    Instance Constructor

    Freely.ZeroM.mk.{u}

    Extends

    Methods

    map : {α β : Type u}  (α  β)  m α  m β
    Inherited from
    1. Monad m
    mapConst : {α β : Type u}  α  m β  m α
    Inherited from
    1. Monad m
    pure : {α : Type u}  α  m α
    Inherited from
    1. Monad m
    seq : {α β : Type u}  m (α  β)  (Unit  m α)  m β
    Inherited from
    1. Monad m
    seqLeft : {α β : Type u}  m α  (Unit  m β)  m α
    Inherited from
    1. Monad m
    seqRight : {α β : Type u}  m α  (Unit  m β)  m β
    Inherited from
    1. Monad m
    bind : {α β : Type u}  m α  (α  m β)  m β
    Inherited from
    1. Monad m
    empty : {α : Type u}  m α

    The failed/empty computation.

  • PlusM m — a binary choice in m (left-biased by design)

    🔗type class
    Freely.PlusM.{u} (m : Type u Type u) : Type (u + 1)
    Freely.PlusM.{u} (m : Type u Type u) : Type (u + 1)

    A codomain with a left-biased choice. Typical instances: Option (orElse), Sum ε (prefer ok), lists (concatenate), and writer-like monads.

    Instance Constructor

    Freely.PlusM.mk.{u}

    Extends

    Methods

    map : {α β : Type u}  (α  β)  m α  m β
    Inherited from
    1. ZeroM m
    mapConst : {α β : Type u}  α  m β  m α
    Inherited from
    1. ZeroM m
    pure : {α : Type u}  α  m α
    Inherited from
    1. ZeroM m
    seq : {α β : Type u}  m (α  β)  (Unit  m α)  m β
    Inherited from
    1. ZeroM m
    seqLeft : {α β : Type u}  m α  (Unit  m β)  m α
    Inherited from
    1. ZeroM m
    seqRight : {α β : Type u}  m α  (Unit  m β)  m β
    Inherited from
    1. ZeroM m
    bind : {α β : Type u}  m α  (α  m β)  m β
    Inherited from
    1. ZeroM m
    empty : {α : Type u}  m α
    Inherited from
    1. ZeroM m
    orElse : {α : Type u}  m α  m α  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 -/ namespace Freely universe u /-- A codomain with a distinguished "no result". Typical instances: `Option`. -/ class ZeroM (m : Type u Type u) extends Monad m where /-- The failed/empty computation. -/ empty : {α}, m α instance : ZeroM Option where empty := none instance {m : Type u Type u} {ρ : Type u} [ZeroM m] : ZeroM (ReaderT ρ m) where empty := λ _ => ZeroM.empty instance {m : Type u Type u} {σ : Type u} [ZeroM m] : ZeroM (StateT σ m) where empty := λ _ => ZeroM.empty /-- A codomain with a left-biased choice. Typical instances: `Option` (`orElse`), `Sum ε` (prefer `ok`), lists (concatenate), and writer-like monads. -/ class PlusM (m : Type u Type u) extends ZeroM m where /-- Combine two computations; prefer the left when meaningful. -/ orElse : {α}, m α m α m α /-- Notation for pointwise addition on handlers/codomains. -/ infixl:55 " ⊞ " => PlusM.orElse instance : PlusM Option where orElse x y := x.orElse (fun _ => y) instance {m : Type u Type u} {ρ : Type u} [PlusM m] : PlusM (ReaderT ρ m) where orElse mx my := λ r => PlusM.orElse (mx r) (my r) instance {m : Type u Type u} {σ : Type u} [PlusM m] : PlusM (StateT σ m) where orElse mx my := λ s => PlusM.orElse (mx s) (my s) end Freely

8. Freely.Handler.Algebra🔗

import Freely.Algebra.ZeroPlus import Freely.Handler.Core import Freely.Program.Monad.Freer namespace Freely universe u /-- Annihilating handler: ignores every operation, returns `empty`. -/ def zeroHandler («Σ» m : Type u Type u) [ZeroM m] : «Σ» ⟹ᶠ m := fun {} _ => ZeroM.empty /-- Pointwise “addition” of handlers using `m`’s `orElse`. -/ def hPlus {«Σ» m} [PlusM m] (θ φ : «Σ» ⟹ᶠ m) : «Σ» ⟹ᶠ m := fun {} op => PlusM.orElse (θ.handle op) (φ.handle op) infixl:60 " ⊕ᵃ " => hPlus /-- `[0, φ]` — annihilate the `F`-side, handle the `G`-side with `φ`. -/ def muteLeft {F G m} [ZeroM m] (φ : G ⟹ᶠ m) : (F ⊕ᶠ G) ⟹ᶠ m := copair (zeroHandler (F) (m)) φ /-- `[θ, 0]` — handle the `F`-side with `θ`, annihilate the `G`-side. -/ def muteRight {F G m} [ZeroM m] (θ : F ⟹ᶠ m) : (F ⊕ᶠ G) ⟹ᶠ m := copair θ (zeroHandler (G) (m)) end Freely

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.

import Freely.Signature.Sum namespace Freely /-! # Freely.Handler.Core — natural transformations as effect handlers -/ universe u v w variable (F : Type u Type u) (G : Type u Type u) /-- A natural transformation `«Σ» ⟹ᶠ m` (component-wise `«Σ» β → m β`). -/ class Handler («Σ» : Type u Type u) (m : Type u Type u) where /-- The component at `α` of the natural transformation `θ : «Σ» ⟹ᶠ M`. -/ handle : {β : Type u}, «Σ» β m β /-- Notation for a handler / natural transformation. -/ scoped infixr:65 " ⟹ᶠ " => Handler /-- Handlers compose over coproducts: if `F ⟹ᶠ m` and `G ⟹ᶠ m`, then `(F ⊕ᶠ G) ⟹ᶠ m`. -/ instance {m : Type u Type u} [F ⟹ᶠ m] [G ⟹ᶠm] : (F ⊕ᶠ G) ⟹ᶠ m where handle | ←ᶠ f => Handler.handle f | →ᶠ g => Handler.handle g /-- Copairing of handlers. Given `θ₁ : «Σ₁» ⟹ᶠ M` and `θ₂ : «Σ₂» ⟹ᶠ M`, produce `[θ₁, θ₂] : («Σ₁» ⊕ᶠ «Σ₂») ⟹ᶠ M` with the defining equations `[θ₁, θ₂] ∘ inl = θ₁` and `[θ₁, θ₂] ∘ inr = θ₂`. -/ def copair {F G m : Type u Type u} (θF : F ⟹ᶠ m) (θG : G ⟹ᶠ m) : (F ⊕ᶠ G) ⟹ᶠ m := fun {} x => match x with | ←ᶠ f => θF.handle f | →ᶠ g => θG.handle g /-- Handler for a sum of signatures, implemented as copairing of component handlers. -/ def handleSum {F G m : Type u Type u} (θ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. -/ class Handles («Σ» : Type Type) (m : Type Type) where /-- `θ : «Σ» ⟹ᶠ M` (a natural transformation from the signature to the semantic monad). -/ θ : «Σ» ⟹ᶠ m export Handles (θ) /-- Recursive instance for sums (co-pairing). -/ instance handlesSum {F G m} [Handles F m] [Handles G m] : Handles (F ⊕ᶠ G) m := handleSum (θ («Σ» := F) (m := m)) (θ («Σ» := G) (m := m)) end Freely

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 α.

🔗def
Freely.interpret.{u} {α : Type u} {«Σ» m : Type u Type u} [Monad m] [«Σ» ⟹ᶠ m] : «Σ» α m α
Freely.interpret.{u} {α : Type u} {«Σ» m : Type u Type u} [Monad m] [«Σ» ⟹ᶠ m] : «Σ» α m α

Interpret a «Σ» α program into an m α using a Handler «Σ» m typeclass instance.

🔗def
Freely.interpret'.{u} {«Σ» m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} : «Σ» α m α
Freely.interpret'.{u} {«Σ» m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} : «Σ» α 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))

🔗theorem
Freely.interpret'_pure.{u} {«Σ» m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} (a : α) : θ (Freer.pure a) = pure a
Freely.interpret'_pure.{u} {«Σ» m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} (a : α) : θ (Freer.pure a) = pure a

⟦θ⟧ preserves pure.

🔗theorem
Freely.interpret'_lift.{u} {«Σ» m : Type u Type u} [Monad m] [LawfulMonad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} (s : «Σ» α) : θ (liftFreer s) = Handler.handle s
Freely.interpret'_lift.{u} {«Σ» m : Type u Type u} [Monad m] [LawfulMonad m] (θ : «Σ» ⟹ᶠ m) {α : Type u} (s : «Σ» α) : θ (liftFreer s) = Handler.handle s

⟦θ⟧ on a single freer-node (via liftFreer).

Laws:

  • 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.

import Freely.Program.Monad.Freer import Freely.Handler.Core namespace Freely /-! # Freely.Handler.Interpret — fold a `ℙ «Σ»` program with a handler -/ universe u v w variable (F : Type u Type u) (G : Type u Type u) /-- Interpret a `ℙ «Σ» α` program into an `m α` using a `Handler «Σ» m` typeclass instance. -/ def interpret {α : Type u} {«Σ» : Type u Type u} {m : Type u Type u} [Monad m] [«Σ» ⟹ᶠ 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] def interpret' {«Σ» : Type u Type u} {m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) : {α}, «Σ» α m α := cataFreer (F := «Σ») (m := m) θ.handle notation "⟦" θ "⟧" => interpret' θ /-- ⟦θ⟧ preserves `pure`. -/ @[simp] theorem interpret'_pure {«Σ» : Type u Type u} {m : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) {α} (a : α) : θ (Freer.pure a) = pure a := «Σ»:Type u Type um:Type u Type uinst✝:Monad mθ:«Σ» ⟹ᶠ mα:Type ua:αθ (Freer.pure a) = pure a All goals completed! 🐙 /-- ⟦θ⟧ on a single `freer`-node (via `liftFreer`). -/ @[simp] theorem interpret'_lift {«Σ» : Type u Type u} {m : Type u Type u} [Monad m] [LawfulMonad m] (θ : «Σ» ⟹ᶠ m) {α} (s : «Σ» α) : θ (liftFreer s) = θ.handle s := «Σ»:Type u Type um:Type u Type uinst✝¹:Monad minst✝:LawfulMonad mθ:«Σ» ⟹ᶠ mα:Type us:«Σ» αθ (liftFreer s) = Handler.handle s All goals completed! 🐙 end Freely

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 small CPS signatures Fᵢ : Type u → Type u, the command deriveSend! («Σ») F₁ … Fₖ emits, for every constructor c of every Fᵢ, a definition

@[inline] def «c» := λ (a₁ : _) … (aₙ : _) =>
Freely.send (F := Fᵢ) (R := «Σ») (c a₁ … aₙ id)

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

import Freely.Program.Monad.Member import Lean open Lean Meta Elab Command /-! # 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₂), …]`. -/ partial def collectBinders (ty : Expr) (acc : Array (Name × BinderInfo) := #[]) : Array (Name × BinderInfo) := match ty with | .forallE n _ b bi => -- one Π binder: name = `n`, body = `b`, kind = `bi` collectBinders b (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. -/ private def isExplicit : 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. -/ private def freshName (i : Nat) (n : Name) : Name := if n == Name.anonymous then Name.mkSimple s!"x{i.succ}" else n /-- Extract the last (unqualified) segment of a `Name`. Used to name the generated wrapper as `<ctorLastSeg>ᵀ`. -/ private def lastSeg! : Name String | .str _ s => s | .num p _ => 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.) let env getEnv -- `ids` is a `TSyntaxArray ident` (the `F₁ … Fₖ`). Iterate by index for robustness. for i in [:ids.size] do -- The i-th effect signature identifier (e.g. `Eff.EffLog`) let idStx : TSyntax `ident := ids[i]! let Fname := idStx.getId -- Find inductive metadata for Fᵢ. Fail early if it's not an inductive. let some (.inductInfo I) := env.find? Fname | throwError "deriveSend!: {Fname} is not an inductive." -- For each constructor of Fᵢ (e.g. `log`, `get`, `set`, …) for ctorName in I.ctors do -- Get constructor metadata let some (.ctorInfo ci) := 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 α` let all := collectBinders ci.type if all.isEmpty then continue -- nothing to do for nullary constructors let pre := all.pop -- drop the *last* binder = continuation -- 2) Keep **only explicit** binders to become wrapper arguments. -- This removes implicit `{α}` and instance `[]` binders like environments. let expl : Array (Name × BinderInfo) := pre.filter (fun _, bi => isExplicit bi) -- 3) Choose argument names for the explicit binders (freshen `_` to `x1`, …). let mut argNames : Array Name := #[] for j in [:expl.size] do let (n0, _) := expl[j]! argNames := argNames.push (freshName j n0) -- 4) Build the **term** `c a₁ … aₙ id` -- We quote a term `($ctorId arg1 … argN id)` using syntax quotations. let ctorId := mkIdent ctorName let initApp : TSyntax `term `($ctorId) -- fold over argument names to build `c a₁ … aₙ` let appArgs : TSyntax `term argNames.foldlM (init := initApp) (fun acc n => `($acc $(mkIdent n))) -- finally append the `id` continuation let appWithId : TSyntax `term `($appArgs id) -- 5) Wrap with `Freely.send` to land in the freer monad over Σ: -- `Freely.send (F := Fᵢ) (R := Σ) (c a₁ … aₙ id)` let rhs : TSyntax `term `(Freely.send (F := $(mkIdent Fname)) (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. let mut body : TSyntax `term := rhs for k in List.range argNames.size |>.reverse do let nm := mkIdent argNames[k]! body `(fun ($nm : _) => $body) -- 7) Choose the new name in *current* namespace: `<ctorLastSeg>ᵀ` let newSimple := Name.mkSimple (lastSeg! ctorName) let newName := mkIdent newSimple -- 8) Emit the definition command. No explicit type signature: Lean will infer it, -- generalizing any implicit indices from the constructor it references. let cmd : TSyntax `command `( @[inline] def $newName := ($body) ) elabCommand cmd -- 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 α.

🔗inductive type
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, 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).

Constructors

pure.{u, v} {F : Type u  Type v} {α : Type u} : α   F α

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.

freer.{u, v} {F : Type u  Type v} {α β : Type u} :
  F β  (β   F α)   F α

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 Σ α.

🔗def
Freely.liftFreer.{u, v} {F : Type u Type v} {α : Type u} (eff : F α) : F α
Freely.liftFreer.{u, v} {F : Type u Type v} {α : Type u} (eff : F α) : F α

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:

  1. 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).

  2. 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.

🔗def
Freely.cataFreer.{u, v, w} {F : Type u Type v} {m : Type u Type w} [Monad m] (algebra : {β : Type u} F β m β) {α : Type u} : F α m α
Freely.cataFreer.{u, v, w} {F : Type u Type v} {m : Type u Type w} [Monad m] (algebra : {β : Type u} F β m β) {α : Type u} : F α m α

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.

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/bind do 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

namespace Freely /-! # Freely.Program.Monad.Freer — free monad on an endofunctor. -/ universe u v w /-- 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)`. -/ inductive Freer (F : Type u -> Type v) (α : Type u) : 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 : α Freer F α /-- 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 : {β : Type u}, F β (β Freer F α) Freer F α /-- Notation for the free monad Freer. Think “`T` over `F`”. -/ scoped notation "ℙ" => 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 : Type u Type v} : Functor ( F) where map g freerF := let rec recMap {α β : Type u} (g : α β) : F α F β := λ program => match program with | .pure x => .pure (g x) | .freer effect continuation => .freer effect (λ y => recMap g (continuation y)) recMap g freerF -- 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 : Type u Type v} : Monad ( F) where pure x := .pure x bind m continuation := let rec recBind {α β : Type u} (m : F α) (continuation : α F β) : F β := match m with | .pure x => continuation x | .freer effect continuation' => .freer effect (λ x' => recBind (continuation' x') continuation) recBind m continuation -- 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. -/ def liftFreer {F : Type u Type v} {α : Type u} (eff : F α) : F α := .freer eff .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. -/ 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 /-! ## 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. -/ scoped notation "ηᵀ" => @pure scoped notation "↑ᵀ" => @liftFreer scoped notation "↓ᵀ" => @cataFreer @[simp] theorem cata_pure {F : Type u Type v} {m : Type u Type w} [Monad m] (algebra : {β}, F β m β) {α} (x : α) : cataFreer (F:=F) (m:=m) algebra (.pure x) = pure x := rfl @[simp] theorem cata_freer {F : Type u Type v} {m : Type u Type w} [Monad m] (algebra : {β}, F β m β) {α β} (effect : F β) (continuation : β F α) : cataFreer (F:=F) (m:=m) algebra (.freer effect continuation) = (algebra effect >>= λ b => cataFreer (F:=F) (m:=m) algebra (continuation b)) := rfl -- ℙ «Σ» is `Freer` in your code @[simp] theorem bind_pure_T {F : Type u Type v} {α β : Type u} (x : α) (continuation : α F β) : Bind.bind (Freer.pure x) continuation = continuation x := rfl @[simp] theorem bind_freer_T {F : Type u Type v} {α β γ : Type u} (effect : F α) (k₁ : α F β) (k₂ : β F γ) : Bind.bind (Freer.freer effect k₁) k₂ = Freer.freer effect (λ a => Bind.bind (k₁ a) k₂) := rfl end Freely

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

🔗type class
Freely.Member.{u} (F R : Type u Type u) : Type (u + 1)
Freely.Member.{u} (F R : Type u Type u) : Type (u + 1)

Member F R 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 [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.

The three instances below form a reflexive–transitive closure along (the right spine of) ⊕ᶠ:

  • memberSelf : F ∈ F

    🔗def
    Freely.memberSelf.{u} (F : Type u Type u) : Member F F
    Freely.memberSelf.{u} (F : Type u Type u) : Member F F

    Reflexivity: F is a member of itself.

  • memberLeft : F ∈ (F ⊕ᶠ R) via left injection

    🔗def
    Freely.memberLeft.{u} (F R : Type u Type u) : Member F (F ⊕ᶠ R)
    Freely.memberLeft.{u} (F R : Type u Type u) : Member F (F ⊕ᶠ R)

    Left inclusion: F is a member of F ⊕ᶠ R via ←ᶠ.

  • memberRight : if F ∈ R then F ∈ (G ⊕ᶠ R) via right injection

    🔗def
    Freely.memberRight.{u} (F G R : Type u Type u) [Member F R] : Member F (G ⊕ᶠ R)
    Freely.memberRight.{u} (F G R : Type u Type u) [Member F R] : Member F (G ⊕ᶠ R)

    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.

import Freely.Program.Monad.Freer import Freely.Signature.Sum namespace Freely /-! # Freely.Program.Monad.Member — “`F` is a summand of `R`” -/ universe u /-- `Member F R` says that `F` can be embedded as a summand of `R`. -/ class Member (F R : Type u Type u) 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. -/ instance memberSelf (F : Type u Type u) : Member F F where inj := id /-- Left inclusion: `F` is a member of `F ⊕ᶠ R` via `←ᶠ`. -/ instance memberLeft (F R : Type u Type u) : Member F (F ⊕ᶠ R) where inj := λ {} x => ←ᶠ x /-- Step-right: if `F` is a member of `R`, then `F` is a member of `G ⊕ᶠ R`. -/ instance memberRight (F G R : Type u Type u) [Member F R] : Member F (G ⊕ᶠ R) where inj := λ {} x => →ᶠ (Member.inj x) /-- Inject an `F`-effect into a larger signature `R` (using `Member`), then lift into the free monad `ℙ R`. -/ @[inline] def send {F R : Type u Type u} [Member F R] {α : Type u} (fx : F α) : R α := ↑ᵀ (Member.inj fx) scoped notation "ιᵀ" => @Freely.send end Freely

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 rebuilding F nodes, which can speed up interpreters and static analyses.

We use lift to enter, lower to exit. map composes cheaply.

/-! Freely/Coyoneda.lean -/ namespace Freely universe u v /-- `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 β`. -/ structure Coyoneda (F : Type u Type v) (α : Type u) : Type (max (u + 1) v) where /-- Hidden source type. -/ β : Type u /-- Underlying request. -/ fi : F β /-- Accumulated map. -/ k : β α namespace Coyoneda variable {F : Type u Type v} {α β γ : Type u} /-- Functor instance for `Coyoneda`: compose post-compositions. Note that Coyoneda is a functor in α without assuming `[Functor F]`. -/ instance : Functor (Coyoneda F) where map f x := x.β, x.fi, f x.k /-- `lower` folds Coyoneda back into `F`. Note that we need `[Functor F]` to push `k` through `fi`. -/ @[inline] def lower [Functor F] (x : Coyoneda F α) : F α := Functor.map x.k x.fi /-- Inject `F` into its Coyoneda expansion. Lift any `F α` into `Coyoneda F α` with identity post-composition. -/ @[inline] def lift (x : F α) : Coyoneda F α := α, x, id /-- Mapping inside `Coyoneda` composes with the stored post-composition. -/ @[simp] theorem map_mk (f : α γ) (x : Coyoneda F α) : (f <$> x) = x.β, x.fi, f x.k := rfl /-- Lowering a concrete `Coyoneda.mk` is just `map` in the underlying functor. -/ @[simp] theorem lower_mk [Functor F] (b : Type u) (fi : F b) (k : b α) : lower (Coyoneda.mk b fi k) = Functor.map k fi := rfl end Coyoneda end Freely

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.

🔗inductive type
Freely.SumF.{u} (F G : Type u Type u) (α : Type u) : Type u
Freely.SumF.{u} (F G : Type u Type u) (α : Type u) : Type u

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.

Constructors

inl.{u} {F G : Type u  Type u} {α : Type u} :
  F α  (F ⊕ᶠ G) α

Left injection of signatures: inl : Σ₁ ⟹ᶠ (Σ₁ ⊕ᶠ Σ₂). Natural in the index; categorically the coproduct inclusion.

inr.{u} {F G : Type u  Type u} {α : Type u} :
  G α  (F ⊕ᶠ G) α

Right injection of signatures: inr : Σ₂ ⟹ᶠ (Σ₁ ⊕ᶠ Σ₂). Natural in the index; categorically the coproduct inclusion.

🔗def
Freely.instanceFunctorSumF.{u} (F G : Type u Type u) [Functor F] [Functor G] : Functor (F ⊕ᶠ G)
Freely.instanceFunctorSumF.{u} (F G : Type u Type u) [Functor F] [Functor G] : Functor (F ⊕ᶠ G)

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.

namespace Freely /-! # Freely.Signature.Sum — binary coproduct of endofunctors on `Type u` -/ universe u v w variable (F : Type u Type u) (G : Type u Type u) /-- `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. -/ inductive SumF (F G : Type u Type u) (α : Type u) : Type u where /-- Left injection of signatures: `inl : Σ₁ ⟹ᶠ (Σ₁ ⊕ᶠ Σ₂)`. Natural in the index; categorically the coproduct inclusion. -/ | inl : F α SumF F G α -- `←ᶠ ` injects left /-- Right injection of signatures: `inr : Σ₂ ⟹ᶠ (Σ₁ ⊕ᶠ Σ₂)`. Natural in the index; categorically the coproduct inclusion. -/ | inr : G α SumF F G α -- `→ᶠ ` injects right /-- `F ⊕ᶠ G` is the notation for binary coproduct (sum) of `F` and `G` endofunctors on `Type u` -/ scoped infixr:65 " ⊕ᶠ " => SumF /-- ←ᶠ is the notation for left injection -/ scoped notation "←ᶠ" => SumF.inl /-- →ᶠ is the notation for right injection -/ scoped notation "→ᶠ" => SumF.inr /-- Functor structure on the sum, pointwise over the summands. -/ instance instanceFunctorSumF [Functor F] [Functor G] : Functor (SumF F G) where map g -- 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.map g e) | →ᶠ e => →ᶠ (Functor.map g e) end Freely

⁙ «freely-docs»🔗

16. Purpose of «freely-docs»

«freely-docs» uses verso to document «freely», «freely-log», and «freely-tests».

«freely» does not depend on «freely-docs». The dependencies go the other way around: «freely-docs» depends on «freely», «freely-log», and «freely-tests».

Documentation is written in a markdown-like style, but in .lean files. See the corresponding portion of the repository for more details.

⁙ «freely-log»🔗

17. Structure of «freely-log»

«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.

  • Program : tiny examples and idioms.

⁘ FreelyLog🔗

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.

import Freely set_option autoImplicit false open Freely namespace FreelyLog abbrev LoggingSection := String abbrev LogMessage := String /-- Minimal levels with a fixed priority order. -/ inductive LogLevel | debug | info | warn | error deriving DecidableEq, Repr, Inhabited namespace LogLevel @[inline] def priority : LogLevel Nat | .debug => 0 | .info => 1 | .warn => 2 | .error => 3 @[inline] def ge (a b : LogLevel) : Bool := priority a priority b end LogLevel /-- Pure formatter: decorate a message given level and a context stack. -/ @[inline] def fmt (lvl : LogLevel) (msg : String) (ctx : List String := []) : String := let path := if ctx.isEmpty then "" else s!"[{String.intercalate " ⟩⟩⟩ " ctx}] " let tag := match lvl with | .debug => "[debug] " | .info => "[info] " | .warn => "[warn] " | .error => "[error] " s!"{tag}{path}{msg}" /-- Coloring -/ @[noinline] def colored (lvl : LogLevel) (s : String) : String := match lvl with | .debug => termColor .debug <| s | .info => termColor .info <| s | .warn => termColor .warn <| s!"! {s}" | .error => termColor .error <| s!"✗ {s}" where color (lvl : LogLevel) : Int := match lvl with | 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{color lvl}m{str}\x1b[0m" -- structures in the monadic categories / contexts involved in handling expressions /-- Live logging state; can be embedded inside larger states. -/ structure LoggingState where level : LogLevel := .debug -- can be changed (usually at beginning of program) ctx : List LoggingSection := [] -- can be updated to hold the relevant sections stack notes : Array LogMessage := #[] -- can be updated to accumulate messages we want to log / emit deriving Repr, Inhabited, DecidableEq /-- Push a formatted line if it meets the threshold. -/ @[noinline] def emit (lvl : LogLevel) (msg : LogMessage) : LoggingState LoggingState | s => if LogLevel.ge lvl s.level then { s with notes := s.notes.push (colored lvl <| fmt lvl msg s.ctx) } else s /-- host states σ that *contain* a `LoggingState` -/ class HasLoggingState (σ : Type) where getLog : σ LoggingState setLog : LoggingState σ σ -- identity lens when σ ≡ FreelyLog.State instance : HasLoggingState LoggingState where getLog s := s setLog s _ := s end FreelyLog

19. FreelyLog.Handler🔗

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 LoggingState embedded 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.

import Freely import FreelyLog.Core import FreelyLog.Monad import FreelyLog.Program import FreelyLog.Signature import FreelyLog.Util open Freely open FreelyLog namespace FreelyLog variable {σ : Type} {m : Type Type} @[inline] def modifyLog [Monad m] [MonadStateOf σ m] [HasLoggingState σ] (f : LoggingState LoggingState) : m Unit := modify (m := m) (σ := σ) (fun (s : σ) => HasLoggingState.setLog (f (HasLoggingState.getLog s)) s) @[inline] def setLevelM [Monad m] [MonadStateOf σ m] [HasLoggingState σ] (l : LogLevel) : m Unit := modify (m := m) (σ := σ) (fun (s : σ) => let ls := HasLoggingState.getLog s HasLoggingState.setLog ({ ls with level := l }) s) @[inline] def pushM [Monad m] [MonadStateOf σ m] [HasLoggingState σ] (name : String) : m Unit := do -- extend ctx with section name, then announce section modify (m := m) (σ := σ) (fun (s : σ) => let ls := HasLoggingState.getLog s HasLoggingState.setLog ({ ls with ctx := ls.ctx ++ [name] }) s) modifyLog (m := m) (σ := σ) (emit .info s!"{name}") @[inline] def popM [Monad m] [MonadStateOf σ m] [HasLoggingState σ] : m Unit := modify (fun s => let ls := HasLoggingState.getLog s HasLoggingState.setLog ({ ls with ctx := ls.ctx.dropLast }) s) @[inline] def logAtM [Monad m] [MonadStateOf σ m] [HasLoggingState σ] (lvl : LogLevel) (msg : String) : m Unit := modifyLog (m := m) (σ := σ) (emit lvl msg) /-- One *generic* handler that works for any `σ` embedding `Log.LoggingState`. -/ def handleLogGeneric [Monad m] [MonadStateOf σ m] [HasLoggingState σ] : Σ«Log» ⟹ᶠ m := fun {} op => do match op with | .setLevel l k => setLevelM (m := m) (σ := σ) l *> pure (k ()) | .push name k => pushM (m := m) (σ := σ) name *> pure (k ()) | .pop k => popM (m := m) (σ := σ) *> pure (k ()) | .logAt l s k => logAtM (m := m) (σ := σ) l s *> 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 [Monad m] [MonadStateOf σ m] [HasLoggingState σ] : Freely.Handles Log m where θ := handleLogGeneric (m := m) (σ := σ) /-- If you like `interpret` to find a handler via typeclass search. -/ instance handleLogInstance [Monad m] [MonadStateOf σ m] [HasLoggingState σ] : Log ⟹ᶠ m where handle := (handleLogGeneric (m := m) (σ := σ)).handle /-- If you like `interpret` to find a handler via a lens instead. -/ def handleLogWithLensToLoggingState [Monad m] [MonadStateOf σ m] (lens : Lens σ LoggingState) : Log ⟹ᶠ m := fun {} op => do let modifyLog (f : LoggingState LoggingState) : m Unit := modify (fun (s : σ) => lens.set (f (lens.get s)) s) match op with | .setLevel l k => modify (fun s => lens.set ({ lens.get s with level := l }) s) *> pure (k ()) | .push name k => modify (fun s => let ls := lens.get s; lens.set ({ ls with ctx := ls.ctx ++ [name] }) s) *> modifyLog (emit .info s!"{name}") *> pure (k ()) | .pop k => modify (fun s => let ls := lens.get s; lens.set ({ ls with ctx := ls.ctx.dropLast }) s) *> pure (k ()) | .logAt l m k => modifyLog (emit l m) *> pure (k ()) end FreelyLog

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.

import Freely import FreelyLog.Core import FreelyLog.Handler import FreelyLog.Monad import FreelyLog.Signature open Freely open FreelyLog namespace FreelyLog /-- 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] def run {α} {m} [Monad m] (p : Σ«Log» α) (s₀ : LoggingState := {}) : m (α × LoggingState) := do (↓ᵀ (m := LogM m) (F := Σ«Log») (λ x => Handler.handle x) p).run s₀ -- 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. end FreelyLog

21. FreelyLog.Monad🔗

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.

import FreelyLog.Core namespace FreelyLog /-- Default target monad for logging -/ abbrev LogM (m : Type Type) := StateT LoggingState m end FreelyLog

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.

import Freely import FreelyLog.Signature open FreelyLog namespace FreelyLog.«Σ_Log_examples» universe u variable {m : Type -> Type} open «Σ_Log» def exampleLogging : Freely.Freer Σ«Log» Unit := do debug "foo" info "bar" warn "baz" error "boo" end FreelyLog.«Σ_Log_examples»

23. FreelyLog.Signature🔗

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.

import Freely import FreelyLog.Core /-! ### Signature -/ open Freely open FreelyLog set_option autoImplicit false /-! The `Log` / `Σ«Log»` DSL is defined principally by an inductively defined, CPS style functor. -/ namespace FreelyLog /-- CPS-style logging signature. -/ inductive Log (α : Type) : Type where | 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 deriving Inhabited /-- Σ«Log» is just another name for the Log signature / effect -/ scoped notation "Σ«Log»" => Log /-- Functor instance for CPS signature (map through continuation). -/ instance : Functor Σ«Log» where map g | .push sectionName k => .push sectionName (g k) | .pop k => .pop (g k) | .setLevel level k => .setLevel level (g k) | .logAt level message k => .logAt level message (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 «Σ»] [Member Log «Σ»] /-- Push a logging "section" onto the context stack -/ @[inline] def push (name : String) : «Σ» Unit := send (Log.push name id) /-- Pop a logging "section" from the context stack -/ @[inline] def pop : «Σ» Unit := send (Log.pop id) /-- Set the log level threshold (below this severity, logs are not emitted) -/ @[inline] def setLevel (lvl : LogLevel) : «Σ» Unit := send (Log.setLevel lvl id) /-- Log a message at a certain LogLevel -/ @[inline] def logAt (lvl : LogLevel) (msg : LogMessage) : «Σ» Unit := send (Log.logAt lvl msg id) /-- Log a message at debug LogLevel -/ @[inline] def debug (x : LogMessage) : «Σ» Unit := logAt .debug x /-- Log a message at info LogLevel -/ @[inline] def info (x : LogMessage) : «Σ» Unit := logAt .info x /-- Log a message at warn LogLevel -/ @[inline] def warn (x : LogMessage) : «Σ» Unit := logAt .warn x /-- Log a message at error LogLevel -/ @[inline] def error (x : LogMessage) : «Σ» Unit := logAt .error x end «Σ_Log» end FreelyLog

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.

import Freely import FreelyLog.Handler import FreelyLog.Signature open Freely open Freely.DeriveTrace open FreelyLog namespace FreelyLog /-- Emit one debug line into whatever state carries `LoggingState`. -/ @[noinline] def pushDebug {σ m} [Monad m] [MonadStateOf σ m] [HasLoggingState σ] (s : String) : m Unit := do -- Use our log emission mechanisms; gating by current threshold happens inside `emit`. modifyLog (m := m) (σ := σ) (FreelyLog.emit .debug s) pure /-- Turn any base handler into a traced handler whose traces go to freely-log. -/ @[noinline] def tracedToLog {«Σ» m σ} [Monad m] [MonadStateOf σ m] [HasLoggingState σ] [TraceF «Σ»] (base : «Σ» ⟹ᶠ m) : «Σ» ⟹ᶠ m := tracedOf' (F := «Σ» ) (M := m) (log := pushDebug (σ := σ) (m := m)) base end FreelyLog

25. FreelyLog.Util🔗

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.

namespace FreelyLog /-- A `Lens σ τ` is a notion of a pair / product of get and set functions on state `σ` and value `τ`. -/ structure Lens (σ τ : Type) where get : σ τ set : τ σ σ end FreelyLog

⁙ «freely-tests»🔗

⁘ FreelyTests🔗

26. FreelyTests.Suites.Freely🔗

suite : ℙ Σ«Log⊕Test» Unit groups themed sections:

  • equational reasoning (Calc)

  • monad and naturality laws

  • interpreter coherence (↓ᵀ plus handler discovery)

  • handler algebra on sums (copair, zero/plus, routing)

  • Coyoneda identities

main : IO Unit runs the suite with tracing enabled, printing the report.

import Freely import FreelyLog import FreelyTests.Kit.Signature import FreelyTests.Kit.Interpret import FreelyTests.Freely.Calc import FreelyTests.Freely.Coyoneda import FreelyTests.Freely.HandlerPlus import FreelyTests.Freely.Handlers import FreelyTests.Freely.Interpretation import FreelyTests.Freely.MaskRouting import FreelyTests.Freely.MonadLaws import FreelyTests.Freely.Naturality import FreelyTests.Freely.SumHandles open Freely open FreelyTests open FreelyTests.SignatureLogAndTest namespace FreelyTests.Suites.Freely def suite : Σ«Log⊕Test» Unit := do section "freely" <| do section "equational reasoning" <| do FreelyTests.Freely.Calc.all section "monad & naturality laws" <| do FreelyTests.Freely.MonadLaws.all FreelyTests.Freely.Naturality.all section "interpretation (↓ᵀ & coherence)" <| do FreelyTests.Freely.Interpretation.all FreelyTests.Freely.Handlers.all section "handler sum algebra (copair, zero/plus, routing)" <| do FreelyTests.Freely.SumHandles.all FreelyTests.Freely.HandlerPlus.all FreelyTests.Freely.MaskRouting.all section "coyoneda" <| do FreelyTests.Freely.Coyoneda.all def main : IO Unit := runIOTraced <| do suite end FreelyTests.Suites.Freely

26.1. Results

[info]  [freely] freely
[info]  [freely ⟩⟩⟩ equational reasoning] equational reasoning
[info]  [freely ⟩⟩⟩ equational reasoning ⟩⟩⟩ calc-demo] calc-demo
[info]  [freely ⟩⟩⟩ equational reasoning ⟩⟩⟩ calc-demo] ✓ succ (got 8, expected 8)
[info]  [freely ⟩⟩⟩ equational reasoning ⟩⟩⟩ calc-demo] ✓ commutes (got 8, expected 8)
[info]  [freely ⟩⟩⟩ equational reasoning] ✓ right identity (got 4, expected 4)
[info]  [freely ⟩⟩⟩ equational reasoning] ✓ associativity (got 4, expected 4)
[info]  [freely ⟩⟩⟩ monad & naturality laws] monad & naturality laws
[info]  [freely ⟩⟩⟩ monad & naturality laws ⟩⟩⟩ (Freer) monad laws] (Freer) monad laws
[info]  [freely ⟩⟩⟩ monad & naturality laws ⟩⟩⟩ (Freer) monad laws] ✓ left identity (got 6, expected 6)
[info]  [freely ⟩⟩⟩ monad & naturality laws ⟩⟩⟩ (Freer) monad laws] ✓ right identity (got 3, expected 3)
[info]  [freely ⟩⟩⟩ monad & naturality laws ⟩⟩⟩ (Freer) monad laws] ✓ associativity (got 9, expected 9)
[info]  [freely ⟩⟩⟩ monad & naturality laws ⟩⟩⟩ naturality (& map fusion)] naturality (& map fusion)
[info]  [freely ⟩⟩⟩ monad & naturality laws ⟩⟩⟩ naturality (& map fusion)] ✓ θ ∘ map f = map f ∘ θ (toy) (got 8, expected 8)
[info]  [freely ⟩⟩⟩ monad & naturality laws ⟩⟩⟩ naturality (& map fusion)] ✓ ↓ᵀ (map f m) = map f (↓ᵀ m) (got 15, expected 15)
[info]  [freely ⟩⟩⟩ monad & naturality laws ⟩⟩⟩ naturality (& map fusion)] ✓ θ_log naturality (value)
[info]  [freely ⟩⟩⟩ monad & naturality laws ⟩⟩⟩ naturality (& map fusion)] ✓ θ_log naturality (state)
[info]  [freely ⟩⟩⟩ interpretation (↓ᵀ & coherence)] interpretation (↓ᵀ & coherence)
[info]  [freely ⟩⟩⟩ interpretation (↓ᵀ & coherence) ⟩⟩⟩ interpretation (↓ᵀ laws)] interpretation (↓ᵀ laws)
[info]  [freely ⟩⟩⟩ interpretation (↓ᵀ & coherence) ⟩⟩⟩ interpretation (↓ᵀ laws)] ✓ ↓ᵀ ∘ ηᵀ = pure (got 42, expected 42)
[info]  [freely ⟩⟩⟩ interpretation (↓ᵀ & coherence) ⟩⟩⟩ interpretation (↓ᵀ laws)] ✓ ↓ᵀ(↑ᵀ op) = θ op (got 4, expected 4)
[info]  [freely ⟩⟩⟩ interpretation (↓ᵀ & coherence) ⟩⟩⟩ interpretation (↓ᵀ laws)] ✓ ↓ᵀ is monad morphism (bind) (got 14, expected 14)
[info]  [freely ⟩⟩⟩ interpretation (↓ᵀ & coherence) ⟩⟩⟩ handlers] handlers
[info]  [freely ⟩⟩⟩ interpretation (↓ᵀ & coherence) ⟩⟩⟩ handlers ⟩⟩⟩ handler-coherence] handler-coherence
[info]  [freely ⟩⟩⟩ interpretation (↓ᵀ & coherence) ⟩⟩⟩ handlers ⟩⟩⟩ handler-coherence] ✓ value matches (got 7, expected 7)
[info]  [freely ⟩⟩⟩ interpretation (↓ᵀ & coherence) ⟩⟩⟩ handlers ⟩⟩⟩ handler-coherence] ✓ state matches
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing)] handler sum algebra (copair, zero/plus, routing)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ copair-left] copair-left
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ copair-left] ✓ copair-left (one node) (value) (got (), expected ())
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ copair-left] ✓ copair-left (one node) (state)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ copair-right] copair-right
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ copair-right] ✓ copair-right (one node) (value) (got (), expected ())
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ copair-right] ✓ copair-right (one node) (state)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ copair-seq] copair-seq
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ copair-seq] ✓ copair (two nodes, Log then Test) (value) (got (), expected ())
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ copair-seq] ✓ copair (two nodes, Log then Test) (state)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ autosynth] autosynth
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ autosynth] ✓ auto-synthesized Handles ≡ copair (mixed program) (value) (got (), expected ())
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ autosynth] ✓ auto-synthesized Handles ≡ copair (mixed program) (state)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ facade] facade
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ facade] ✓ façade = ιᵀ ∘ inj (value, auto-synth handler) (got (), expected ())
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ facade] ✓ façade = ιᵀ ∘ inj (state, auto-synth handler)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ facade] ✓ façade = ιᵀ ∘ inj (value, copair handler) (got (), expected ())
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ facade] ✓ façade = ιᵀ ∘ inj (state, copair handler)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ handler algebra (zero/plus)] handler algebra (zero/plus)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ handler algebra (zero/plus)] ✓ zero annihilates: ⟦0⟧ = empty (got none, expected none)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ handler algebra (zero/plus)] ✓ interpret (θ ⊕ᵃ φ) = interpret θ ⊞ interpret φ (fail/succeed) (got (some 2), expected (some 2))
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ handler algebra (zero/plus)] ✓ left bias: (θ ⊕ᵃ φ) picks θ when both succeed (got (some 7), expected (some 7))
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ handler algebra (zero/plus)] ✓ value comes from left handler (got (some 7), expected (some 7))
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ handler algebra (zero/plus)] ✓ pointwise across nodes (A then B) (got (some 3), expected (some 3))
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ routing (masking & policy)] routing (masking & policy)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ routing (masking & policy) ⟩⟩⟩ masking/muting via muteRight → Option] masking/muting via muteRight → Option
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ routing (masking & policy) ⟩⟩⟩ masking/muting via muteRight → Option] ✓ Σ«Tick» then Σ«Tock» is muted (none) (got none, expected none)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ routing (masking & policy) ⟩⟩⟩ masking/muting via muteRight → Option] ✓ Σ«Tock» then Σ«Tick» is muted (none) (got none, expected none)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ routing (masking & policy) ⟩⟩⟩ masking/muting via muteRight → Option] ✓ orders equal under muting (got none, expected none)
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ routing (masking & policy) ⟩⟩⟩ routing via handler algebra] routing via handler algebra
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ routing (masking & policy) ⟩⟩⟩ routing via handler algebra] ✓ masked-sum ≡ if/else
[info]  [freely ⟩⟩⟩ handler sum algebra (copair, zero/plus, routing) ⟩⟩⟩ routing (masking & policy) ⟩⟩⟩ routing via handler algebra] ✓ expected payloads
[info]  [freely ⟩⟩⟩ coyoneda] coyoneda
[info]  [freely ⟩⟩⟩ coyoneda ⟩⟩⟩ lower = map] lower = map
[info]  [freely ⟩⟩⟩ coyoneda ⟩⟩⟩ lower = map] ✓ lower agrees with map (got (some 4), expected (some 4))
[info]  [freely ⟩⟩⟩ coyoneda ⟩⟩⟩ map fusion via lower] map fusion via lower
[info]  [freely ⟩⟩⟩ coyoneda ⟩⟩⟩ map fusion via lower] ✓ map fusion through lower (got (some 31), expected (some 31))
[info]  [freely ⟩⟩⟩ coyoneda ⟩⟩⟩ lower ∘ lift = id (Option)] lower ∘ lift = id (Option)
[info]  [freely ⟩⟩⟩ coyoneda ⟩⟩⟩ lower ∘ lift = id (Option)] ✓ none round-trips (got none, expected none)
[info]  [freely ⟩⟩⟩ coyoneda ⟩⟩⟩ lower ∘ lift = id (Option)] ✓ some round-trips (got (some 42), expected (some 42))
tests: 42, failed: 0, xfail: 0, upass: 0

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.

import Freely import FreelyTests.Fixtures.Signature.Tick open Freely open scoped Freely namespace FreelyTests.Fixtures.Tick -- Handlers for Id monad /-- Algebra into `Id`: increment and feed the continuation. -/ def α_tick : {β}, SignatureTick β Id β := λ | .tick n continuation => 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 β := λ | .tick n continuation => some (continuation (n+1)) /-- Handle `F` by succeeding (`some`) and incrementing. -/ def θ_tick_Option : Σ«Tick» ⟹ᶠ Option := α_tick_Option end FreelyTests.Fixtures.Tick

28. FreelyTests.Fixtures.Handlers.Tock🔗

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.

import Freely import FreelyTests.Fixtures.Signature.Tock open Freely open scoped Freely namespace FreelyTests.Fixtures.Tock /-- Algebra into `Id`: feed the unchanged value into the continuation. -/ def α_tock : {β}, SignatureTock β Id β | _, .tock n continuation => continuation n /-- Same as a `Handler`. -/ def θ_tock : SignatureTock ⟹ᶠ Id := α_tock end FreelyTests.Fixtures.Tock

29. FreelyTests.Fixtures.Signature.Tick - signature Σ«Tick»🔗

A minimal CPS signature Σ«Tick» with one operation:

tick : Nat → (Nat → α) → Σ«Tick» α

Semantics: “increment by 1, then continue”. We also provide:

  • Functor instance: acts by post-composing on the continuation.

  • Smart constructor «tick» : Nat → ℙ Σ«Tick» Nat (lift a single node).

This signature is used as a tractable carrier for equational and categorical laws about ↓ᵀ (free-monad catamorphism), handler composition, and sums.

import Freely open Freely open scoped Freely namespace FreelyTests.Fixtures.Tick /-- “tock” effect: return `n + 1` via the continuation. -/ inductive SignatureTick (α : Type) where | tick : Nat (Nat α) SignatureTick α deriving instance Inhabited for SignatureTick scoped notation "Σ«Tick»" => SignatureTick instance : Functor SignatureTick where map g | .tick n k => .tick n (g k) @[inline] def tick (n : Nat) : Σ«Tick» Nat := ↑ᵀ (SignatureTick.tick n id) end FreelyTests.Fixtures.Tick

30. FreelyTests.Fixtures.Signature.Tock - signature Σ«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.

import Freely open Freely open scoped Freely namespace FreelyTests.Fixtures.Tock /-- “tock” effect: return `n` unchanged via the continuation. -/ inductive SignatureTock (α : Type) where | tock : Nat (Nat α) SignatureTock α deriving instance Inhabited for SignatureTock scoped notation "Σ«Tock»" => SignatureTock instance : Functor SignatureTock where map g | .tock n k => .tock n (g k) @[inline] def tock (n : Nat) : Σ«Tock» Nat := ↑ᵀ (SignatureTock.tock n id) end FreelyTests.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.

import FreelyTests.Fixtures.Handlers.Tick import FreelyTests.Fixtures.Signature.Tick

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.

import FreelyTests.Fixtures.Tick import FreelyTests.Fixtures.Tock open Freely namespace FreelyTests.Fixtures.TickTock abbrev SignatureTickTock := FreelyTests.Fixtures.Tick.SignatureTick ⊕ᶠ FreelyTests.Fixtures.Tock.SignatureTock scoped notation "Σ«Tick⊕Tock»" => SignatureTickTock -- This generates «tick» and «tock» that build nodes directly in the sum: deriveSend! (SignatureTickTock) FreelyTests.Fixtures.Tick.SignatureTick FreelyTests.Fixtures.Tock.SignatureTock -- #check «tick» -- #check «tock» end FreelyTests.Fixtures.TickTock

33. FreelyTests.Fixtures.Tock🔗

33.1. FreelyTests.Fixtures.Tock

Aggregation module for the Σ«Tock» signature and its canonical Id handler.

import FreelyTests.Fixtures.Handlers.Tock import FreelyTests.Fixtures.Signature.Tock

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.

import Freely import FreelyTests.Kit.Signature import FreelyTests.Fixtures.Tick open Freely open FreelyTests.SignatureLogAndTest open FreelyTests.Fixtures.Tick namespace FreelyTests.Freely.Calc universe u structure Step (α : Type u) where name : String lhs : α rhs : α /-- Run named equalities in `Id`. -/ def calcId {α} [DecidableEq α] [ToString α] (steps : List (Step α)) : Σ«Log⊕Test» Unit := do for s in steps do expectEq s.name s.lhs s.rhs /-- Run named equalities whose payloads are `Id α`. -/ def calcId_run {α} [DecidableEq α] [ToString α] (steps : List (Step (Id α))) : Σ«Log⊕Test» Unit := do for s in steps do expectEq s.name (Id.run s.lhs) (Id.run s.rhs) /-- Compare programs *after* interpretation by `θ : «Σ» ⟹ᶠ m` and then `run`. -/ def calcInterp {«Σ» : Type u Type u} {m : Type u Type u} {ρ : Type u Type u} [Monad m] (θ : «Σ» ⟹ᶠ m) (run : {α}, m α ρ α) {α : Type u} [DecidableEq (ρ α)] [ToString (ρ α)] (steps : List (Step ( «Σ» α))) : Σ«Log⊕Test» Unit := do for s in steps do expectEq s.name (run (θ s.lhs)) (run (θ s.rhs)) /-- Pretty constructor: `⟦"name"⟧ lhs ≃ rhs`. -/ scoped notation "⟪" n "⟫ " lhs " ≃ " rhs => FreelyTests.Freely.Calc.Step.mk (α := _) n lhs rhs /-- Demo: two named links under `Id`. -/ def demoId : Σ«Log⊕Test» Unit := do let a := (7 : Nat) let b := a + 1 section "calc-demo" <| do calcId [ "succ" (a + 1) b , "commutes" (1 + a) b ] /-- Demo: associativity ∘ right-identity as program equalities folded by `theta'` then `Id.run`. -/ def assoc_rightId_chain : Σ«Log⊕Test» Unit := do let m : SignatureTick Nat := tick 1 let continuation : Nat SignatureTick Nat := λ n => tick (n+1) calcInterp (θ := θ_tick) (run := Id.run) [ "right identity" (m >>= continuation) >>= (λ z => ηᵀ z) (m >>= continuation) , "associativity" (m >>= continuation) (m >>= (λ x => continuation x >>= (λ z => ηᵀ z))) ] def all : Σ«Log⊕Test» Unit := do demoId assoc_rightId_chain end FreelyTests.Freely.Calc

35. FreelyTests.Freely.Coyoneda🔗

Property tests for the Coyoneda construction:

  • 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.

import Freely import FreelyLog import FreelyTests.Kit.Signature set_option autoImplicit false open Freely open FreelyTests.SignatureLogAndTest namespace FreelyTests.Freely.Coyoneda /-- `lower = map` on a concrete node, via `map ∘ lift`. -/ def lower_eq_map : Σ«Log⊕Test» Unit := section "lower = map" <| do let fi : Option Nat := some 3 let k : Nat Nat := (·+ 1) -- Build a Coyoneda node as `map k (lift fi)`. let t : Freely.Coyoneda Option Nat := Functor.map k (Freely.Coyoneda.lift fi) -- (these are the functions under test) -- lift/map/lower live here: -- Freely.Coyoneda.lift / .map / .lower (see core definitions) let lhs : Option Nat := Freely.Coyoneda.lower t let rhs : Option Nat := Functor.map k fi expectEq "lower agrees with map" lhs rhs /-- Map fusion at the signature layer, witnessed after `lower`. -/ def fusion : Σ«Log⊕Test» Unit := section "map fusion via lower" <| do let fi : Option Nat := some 7 let f : Nat Nat := (·* 3) let g : Nat Nat := (·+ 10) -- left: lower (map g (map f (lift fi))) let lhs : Option Nat := Freely.Coyoneda.lower (Functor.map g (Functor.map f (Freely.Coyoneda.lift fi))) -- right: map (g ∘ f) fi let rhs : Option Nat := Functor.map (g f) fi expectEq "map fusion through lower" lhs rhs /-- `lower ∘ lift = id` (checked on `Option` to avoid needing functor laws). -/ def lower_lift_id : Σ«Log⊕Test» Unit := section "lower ∘ lift = id (Option)" <| do let a₁ : Option Nat := none let a₂ : Option Nat := some 42 let lhs₁ := Freely.Coyoneda.lower (Freely.Coyoneda.lift a₁) let lhs₂ := Freely.Coyoneda.lower (Freely.Coyoneda.lift a₂) expectEq "none round-trips" lhs₁ a₁ expectEq "some round-trips" lhs₂ a₂ /-- Suite entry point for Coyoneda tests. -/ def all : Σ«Log⊕Test» Unit := do lower_eq_map fusion lower_lift_id end FreelyTests.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.

import Freely import Freely.Handler.Algebra -- zeroHandler, hPlus (⊕ᵃ) import Freely.Handler.Interpret -- interpret', ⟦θ⟧ import FreelyTests.Kit.Signature open Freely open FreelyTests open FreelyTests.SignatureLogAndTest namespace FreelyTests.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 inductive SignatureKV (α : Type) where | get : String (Nat α) SignatureKV α scoped notation "Σ«KV»" => SignatureKV instance : Functor Σ«KV» where map g | .get k cont => .get k (g cont) @[inline] def get (key : String) : Σ«KV» Nat := ↑ᵀ (SignatureKV.get key id) -- two complementary handlers into Option def θ_A : Σ«KV» ⟹ᶠ Option := fun | .get key k => if key = "A" then pure (k 1) -- succeed with 1 else (none : Option _) -- fail def θ_B : Σ«KV» ⟹ᶠ Option := fun | .get key k => if key = "B" then pure (k 2) -- succeed with 2 else (none : Option _) /-! ## 1. Annihilation `zeroHandler` ignores every operation and returns `ZeroM.empty` (here: `none`). -/ open FreelyTests.SignatureLogAndTest def annihilation : Σ«Log⊕Test» Unit := do let p : Σ«KV» Nat := get "A" let lhs : Option Nat := (zeroHandler Σ«KV» Option) p let rhs : Option Nat := none expectEq "zero annihilates: ⟦0⟧ = empty" lhs rhs /-! ## 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. -/ def choice_pointwise_fail_then_succeed : Σ«Log⊕Test» Unit := do let p : Σ«KV» Nat := get "B" -- θ_A fails, θ_B succeeds let lhs := (θ_A ⊕ᵃ θ_B) p let rhs := PlusM.orElse (θ_A p) (θ_B p) expectEq "interpret (θ ⊕ᵃ φ) = interpret θ ⊞ interpret φ (fail/succeed)" lhs rhs def choice_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 (k 7) let θ_R : Σ«KV» ⟹ᶠ Option := fun | .get _ k => pure (k 9) let p : Σ«KV» Nat := do get "Z" let lhs := (θ_L ⊕ᵃ θ_R) p let rhs := (θ_L p) (θ_R p) expectEq "left bias: (θ ⊕ᵃ φ) picks θ when both succeed" lhs rhs expectEq "value comes from left handler" lhs (some 7) /-! ## 3. Sequential program shows *pointwise* combination across nodes `θ_A` covers `"A"`, `θ_B` covers `"B"`. With `⊕ᵃ`, each node falls back independently. -/ def choice_pointwise_sequential : Σ«Log⊕Test» Unit := do let p : Σ«KV» Nat := do let a get "A" -- picked from θ_A let b get "B" -- picked from θ_B (θ_A fails here) pure (a + b) -- expect 1 + 2 = 3 let lhs := (θ_A ⊕ᵃ θ_B) p expectEq "pointwise across nodes (A then B)" lhs (some 3) def all : Σ«Log⊕Test» Unit := do section "handler algebra (zero/plus)" <| do annihilation choice_pointwise_fail_then_succeed choice_pointwise_both_succeed_left_biased choice_pointwise_sequential end FreelyTests.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.

import Freely import FreelyLog import FreelyTests.Fixtures.Tick import FreelyTests.Kit.Signature import FreelyTests.Kit.State import FreelyTests.Kit.Monad open Freely open FreelyLog open FreelyLog.«Σ_Log» open FreelyTests.Fixtures.Tick open FreelyTests.SignatureLogAndTest namespace FreelyTests.Freely.Handlers /-- A handler for `SignatureTick` into `TestM` that *logs* on each tick. -/ def θ_tick_and_log : SignatureTick ⟹ᶠ TestM := fun | .tick n k => do emit .info s!"tick {n}" pure (k (n+1)) /-- Give a `Handles` instance so `interpret` can use it. -/ instance : Handles SignatureTick TestM where θ := θ_tick_and_log /-- `interpret` via the instance equals an explicit catamorphism with the same algebra. -/ def interpret_eq_cata : Σ«Log⊕Test» Unit := do section "handler-coherence" <| do -- a small program that triggers the handler multiple times let prog : SignatureTick Nat := do let a tick 1 let b 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) let m₁ : TestM Nat := θ_tick_and_log prog --interpret' («Σ» := SignatureTick) (m := M) prog (θ := θ_tick_and_log) -- (2) direct catamorphism with the same algebra let m₂ : TestM Nat := ↓ᵀ (F := SignatureTick) (m := TestM) θ_tick_and_log.handle prog let r₀ : Report := {} let (v₁, s₁) := Id.run <| m₁.run r₀ let (v₂, s₂) := Id.run <| m₂.run r₀ expectEq "value matches" v₁ v₂ expectTrue "state matches" (decide (s₁ = s₂)) def all : Σ«Log⊕Test» Unit := do section "handlers" <| do interpret_eq_cata end FreelyTests.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 θ.

import Freely import FreelyTests.Kit.Signature import FreelyTests.Fixtures.Tick open Freely open FreelyTests.SignatureLogAndTest open FreelyTests.Fixtures.Tick namespace FreelyTests.Freely.Interpretation /-- `↓ᵀ ∘ ηᵀ = id` -/ def interpretUnit : Σ«Log⊕Test» Unit := do let x := 42 let lhs := Id.run <| ↓ᵀ (F := Σ«Tick») (m := Id) α_tick (ηᵀ x) expectEq "↓ᵀ ∘ ηᵀ = pure" lhs x /-- `↓ᵀ` on a single op equals applying the algebra. -/ def interpretOp : Σ«Log⊕Test» Unit := do let op := .tick 3 id let lhs := Id.run <| ↓ᵀ (F := Σ«Tick») (m := Id) α_tick (↑ᵀ op) let rhs := Id.run <| α_tick op expectEq "↓ᵀ(↑ᵀ op) = θ op" lhs rhs /-- `↓ᵀ` respects bind (homomorphism to the target monad). -/ def interpBind : Σ«Log⊕Test» Unit := do let m : Σ«Tick» Nat := tick 2 let continuation : Nat Σ«Tick» Nat := λ n => tick (n+10) let lhs := Id.run <| ↓ᵀ (F := Σ«Tick») (m := Id) α_tick (m >>= continuation) let rhs := Id.run <| (↓ᵀ (F := Σ«Tick») (m := Id) α_tick m) >>= λ a => ↓ᵀ (F := Σ«Tick») (m := Id) α_tick (continuation a) expectEq "↓ᵀ is monad morphism (bind)" lhs rhs def all : Σ«Log⊕Test» Unit := do section "interpretation (↓ᵀ laws)" <| do interpretUnit interpretOp interpBind end FreelyTests.Freely.Interpretation

39. FreelyTests.Freely.MaskRouting — masking/muting and predicate routing for handlers

Two themes:

  1. 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.

  2. 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 ↓ᵀ.)

import Freely import FreelyLog import FreelyTests.Fixtures.Tick import FreelyTests.Fixtures.Tock import FreelyTests.Fixtures.TickTock import FreelyTests.Freely.Calc import FreelyTests.Kit.Signature set_option autoImplicit false open Freely open FreelyTests open FreelyTests.Fixtures.TickTock open FreelyTests.Freely.Calc open FreelyTests.SignatureLogAndTest namespace FreelyTests.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] def runOpt {α} {«Σ» : Type Type} [Functor «Σ»] (θ : «Σ» ⟹ᶠ Option) (p : «Σ» α) : Option α := ↓ᵀ (F := «Σ») (m := Option) θ.handle p /-- Small programs over `Σ«Tick» ⊕ᶠ Σ«Tock»` that interleave nodes. -/ def prog_tick_then_tock : Σ«Tick⊕Tock» Nat := do let a tick 0 let b tock a pure (a + b) def prog_tock_then_tick : Σ«Tick⊕Tock» Nat := do let a tock 5 let b tick a pure (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`. -/ def muteRight_check : Σ«Log⊕Test» Unit := section "masking/muting via muteRight → Option" <| do let θ_proj : Σ«Tick⊕Tock» ⟹ᶠ Option := muteRight FreelyTests.Fixtures.Tick.θ_tick_Option let r1 := runOpt θ_proj prog_tick_then_tock let r2 := runOpt θ_proj prog_tock_then_tick expectEq "Σ«Tick» then Σ«Tock» is muted (none)" r1 (none : Option Nat) expectEq "Σ«Tock» then Σ«Tick» is muted (none)" r2 (none : Option Nat) -- 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. -/ inductive RouteF (α : Type) : Type where | req : (good? : Bool) (n : Nat) (Nat α) RouteF α deriving instance Inhabited for RouteF instance : Functor RouteF where map g | .req b n k => .req b n (g k) /-- "Good" and "Bad" `RouteF` handlers. We return distinct markers. -/ def θ_good : RouteF ⟹ᶠ Option := λ | .req _ n k => some (k (n + 100)) def θ_bad : RouteF ⟹ᶠ Option := λ | .req _ n k => some (k (n + 200)) /-- Predicate on requests: route by the tag. -/ def isGood : {α : Type} RouteF α Bool | _, .req b _ _ => b /-- Mask a handler with a request predicate (mute when predicate is false). -/ def θ_maskBy (p : {α : Type} RouteF α Bool) (θ : RouteF ⟹ᶠ Option) : RouteF ⟹ᶠ Option := λ op => if p op then θ.handle op else (ZeroM.empty) /-- The routing algebra expressed as "algebra only" (no program rewrite). -/ def θ_route : RouteF ⟹ᶠ Option := (θ_maskBy isGood θ_good) ⊕ᵃ (θ_maskBy (λ op => ! isGood op) θ_bad) /-- The same routing, written directly. -/ def θ_if : RouteF ⟹ᶠ Option := λ op => if isGood op then θ_good.handle op else θ_bad.handle op /-- A tiny program that hits both paths once and returns their pair. -/ def prog_route : RouteF (Nat × Nat) := do let a (↑ᵀ (RouteF.req true 1 id)) let b (↑ᵀ (RouteF.req false 2 id)) pure (a, b) /-! ## Check 2: policy swap equality (routing via handler algebra) The masked-sum algebra equals the straightforward if/else algebra on programs. -/ def policySwap_check : Σ«Log⊕Test» Unit := section "routing via handler algebra" <| do let r_mask : Option (Nat × Nat) := runOpt θ_route prog_route let r_if : Option (Nat × Nat) := runOpt θ_if prog_route expectTrue "masked-sum ≡ if/else" (decide (r_mask = r_if)) expectTrue "expected payloads" (decide (r_mask = some (101, 202))) /-- Suite entry point for this file. -/ def all : Σ«Log⊕Test» Unit := section "routing (masking & policy)" <| do muteRight_check policySwap_check end FreelyTests.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”.

import Freely import FreelyLog import FreelyTests.Kit.Signature import FreelyTests.Fixtures.Tick import FreelyTests.Freely.Calc import FreelyTests.Freely.Interpretation open Freely open FreelyTests.Freely.Calc open FreelyTests.SignatureLogAndTest open FreelyTests.Fixtures.Tick namespace FreelyTests.Freely.MonadLaws open FreelyTests.Freely.Interpretation def leftId : Σ«Log⊕Test» Unit := do let x := 5 let continuation : Nat SignatureTick Nat := λ n => tick n calcInterp (θ := θ_tick) (run := Id.run) [ "left identity" ((ηᵀ x) >>= continuation) (continuation x) ] def rightId : Σ«Log⊕Test» Unit := do let m : SignatureTick Nat := tick 2 calcInterp (θ := θ_tick) (run := Id.run) [ "right identity" (m >>= (λ z => ηᵀ z)) m ] def assoc : Σ«Log⊕Test» Unit := do let m : SignatureTick Nat := tick 1 let k : Nat SignatureTick Nat := λ n => tick (n+1) let h : Nat SignatureTick Nat := λ n => tick (n*2) calcInterp (θ := θ_tick) (run := Id.run) [ "associativity" ((m >>= k) >>= h) (m >>= (λ x => k x >>= h)) ] def all : Σ«Log⊕Test» Unit := do section "(Freer) monad laws" <| do leftId rightId assoc end FreelyTests.Freely.MonadLaws

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.

import Freely import FreelyTests.Kit.Handler import FreelyTests.Fixtures.Tick import FreelyTests.Freely.Interpretation open Freely open FreelyTests.SignatureLogAndTest open FreelyTests.Fixtures.Tick namespace FreelyTests.Freely.Naturality /-- θ naturality for the toy signature into Id. -/ def θ_natural : Σ«Log⊕Test» Unit := do let f : Nat Nat := (·* 2) let e : SignatureTick Nat := .tick 3 id let lhs := Id.run <| α_tick (Functor.map f e) let rhs := Id.run <| f <$> α_tick e expectEq "θ ∘ map f = map f ∘ θ (toy)" lhs rhs /-- Fusion of map with `↓ᵀ`. -/ def freer_map_fusion : Σ«Log⊕Test» Unit := do let f : Nat Nat := (·+ 10) let m : SignatureTick Nat := do let a ↑ᵀ (SignatureTick.tick 1 id) ↑ᵀ (SignatureTick.tick (a + 2) id) let lhs := Id.run <| ↓ᵀ (F := SignatureTick) (m := Id) α_tick (Functor.map f m) let rhs := Id.run <| f <$> ↓ᵀ (F := SignatureTick) (m := Id) α_tick m expectEq "↓ᵀ (map f m) = map f (↓ᵀ m)" lhs rhs /-- Natural transformation law for the logging handler θ_log : Log ⇒ TestM. -/ def θ_log_natural : Σ«Log⊕Test» Unit := do let e : FreelyLog.Log Unit := FreelyLog.Log.setLevel .debug id let f : Unit Unit := id let r₀ : Report := {} let lhs := Id.run <| (FreelyTests.θ_log.handle (Functor.map f e)).run r₀ let rhs := Id.run <| (f <$> FreelyTests.θ_log.handle e).run r₀ expectTrue "θ_log naturality (value)" (decide (lhs.fst = rhs.fst)) expectTrue "θ_log naturality (state)" (decide (lhs.snd = rhs.snd)) def all : Σ«Log⊕Test» Unit := do section "naturality (& map fusion)" <| do θ_natural freer_map_fusion θ_log_natural end FreelyTests.Freely.Naturality

42. FreelyTests.Freely.SumHandles — auto-synthesized handlers vs explicit copairs🔗

We compare two ways to interpret Σ«Log⊕Test»:

  1. Auto: use Handles instances and ⟦θ⟧ (class-synthesized).

  2. 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.

import Freely import FreelyLog import FreelyTests.Kit.Signature import FreelyTests.Kit.Handler import FreelyTests.Kit.Interpret set_option autoImplicit false open Freely open FreelyLog open FreelyTests open FreelyTests.SignatureLogAndTest namespace FreelyTests.Freely.SumHandles /-- A tiny helper to run the same program two ways and compare results. -/ @[noinline] def expectEqRuns (msg : String) (p : Σ«Log⊕Test» Unit) : Σ«Log⊕Test» Unit := do let (v₁, s₁) := FreelyTests.run p -- class-synthesized handler let (v₂, s₂) := FreelyTests.run' p -- explicit copair handler expectEq s!"{msg} (value)" v₁ v₂ expectTrue s!"{msg} (state)" (decide (s₁ = s₂)) /-- Single-node on the *left* summand (Log). -/ def copair_left : Σ«Log⊕Test» Unit := do section "copair-left" <| do -- one Log node let p : Σ«Log⊕Test» Unit := do info "hello" expectEqRuns "copair-left (one node)" p /-- Single-node on the *right* summand (Test). -/ def copair_right : Σ«Log⊕Test» Unit := do section "copair-right" <| do -- one Test node let p : Σ«Log⊕Test» Unit := do assert "ok" true expectEqRuns "copair-right (one node)" p /-- Two nodes (one from each side) to exercise the `↓ᵀ` recursion over sums. -/ def copair_sequence : Σ«Log⊕Test» Unit := do section "copair-seq" <| do let p : Σ«Log⊕Test» Unit := do info "begin" assert "check" true expectEqRuns "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.) -/ def autosynth_coincides_with_copair : Σ«Log⊕Test» Unit := do section "autosynth" <| do -- a slightly longer mix just to shake the tree let p : Σ«Log⊕Test» Unit := do info "a" assert "b" true info "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. -/ def facade_correctness : Σ«Log⊕Test» Unit := do section "facade" <| do let msg := "facade" let ok := true -- façade-built program let p₁ : Σ«Log⊕Test» Unit := do assert msg ok -- raw CPS node injected then lifted let p₂ : Σ«Log⊕Test» Unit := do ιᵀ (F := Test) (R := SignatureLogAndTest) (Member.inj (Test.assert msg ok (fun _ => ()))) -- auto-synthesized handler vs explicit copair, comparing components (not pairs) let (v₁, s₁) := run p₁ {} let (v₂, s₂) := run p₂ {} 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. -/ def all : Σ«Log⊕Test» Unit := do copair_left copair_right copair_sequence autosynth_coincides_with_copair facade_correctness end FreelyTests.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.

import Freely import FreelyLog import FreelyTests.Kit.State import FreelyTests.Kit.Monad import FreelyTests.Kit.Signature open Freely open FreelyLog namespace FreelyTests /-! ## 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-/ def reportLogLens : Lens Report LoggingState := { get := (·.logS), set := λ ls r => { r with logS := 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 => do match op with | .assert msg ok k => do modify (fun r => { r with total := r.total + 1 }) if ok then emit .info s!"{colored .info "✓"} {msg}" else modify (fun r => { r with failed := r.failed + 1 }) emit .error s!"{colored .error "✗"} {msg}" pure (k ()) | .xassert msg ok k => do if ok then modify (fun r => { r with ups := r.ups + 1 }) emit .error s!"{colored .error "(!) unexpected pass"} {msg}" else modify (fun r => { r with xfails := r.xfails + 1 }) emit .info s!"{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 => match s with | ←ᶠ l => θ_log.handle l | →ᶠ t => θ_test.handle t open FreelyLog /-- 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 : Handles Test TestM where θ := θ_test /-- This instance is coming for free from `Freely.Handler` -/ instance : Handles SignatureLogAndTest TestM := inferInstance end FreelyTests

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.

import Freely import FreelyLog import FreelyTests.Kit.State import FreelyTests.Kit.Handler import FreelyTests.Kit.Monad import FreelyTests.Kit.Signature import FreelyTests.Kit.Trace open Freely open FreelyLog open FreelyTests.SignatureLogAndTest namespace FreelyTests /-- 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] def run {α} (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] def run' {α} (program : Σ«Log⊕Test» α) (r₀ : Report := {}) : (α × Report) := runTestM (θ_log_and_test program : TestM α) r₀ @[noinline] def runIO (program : Σ«Log⊕Test» α) (r₀ : Report := {}) : IO Unit := do let (_result, report) := run program r₀ IO.println (testsOutput report) @[noinline] def runIO' (program : Σ«Log⊕Test» α) (r₀ : Report := {}) : IO Unit := do let (_result, report) := run' program r₀ IO.println (testsOutput report) @[noinline] def runIOTraced (program : Σ«Log⊕Test» α) (r₀ : Report := {}) : IO Unit := do let (_result, report) := run program r₀ IO.println (testsOutput report) @[noinline] def runIOTraced' (program : Σ«Log⊕Test» α) (r₀ : Report := {}) : IO Unit := do let (_result, report) := run' program r₀ IO.println (testsOutput report) end FreelyTests

45. FreelyTests.Kit.Monad — the test carrier🔗

Defines the target monad and helper combinators:

  • TestM := StateT Report Id

  • runTestM : TestM α → Report → (α × Report)

  • 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».

import FreelyLog import FreelyTests.Kit.State set_option autoImplicit false open FreelyLog namespace FreelyTests /-- All handlers will target this state monad. -/ abbrev TestM := StateT Report Id @[noinline] def runTestM {α} (m : TestM α) (r₀ : Report := {}) : (α × Report) := Id.run <| do (m : TestM α).run r₀ instance : Monad TestM := inferInstance @[inline] def withLog (f : LoggingState LoggingState) : TestM Unit := modify (fun r => { r with logS := f r.logS }) /-- Convenience: append a line via the freely-log formatter & threshold. -/ @[inline] def emit (lvl : LogLevel) (msg : String) : TestM Unit := withLog (FreelyLog.emit lvl msg) end FreelyTests

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.

import Freely import FreelyLog import FreelyTests.Kit.Interpret import FreelyTests.Kit.Signature open Freely open FreelyLog.«Σ_Log» open FreelyTests.SignatureLogAndTest namespace FreelyTests.«Σ_test_and_log_examples» def demoSuite : Σ«Log⊕Test» Unit := do setLevel .debug push "arith" push "checks" info "starting arithmetic checks" warn "some warning..." expectEq "1+1=2" (1+1) (2 : Nat) expectEq "2*3=6" (2*3) (6 : Nat) pop pop push "booleans" expectTrue "true && true" (true && true) xassert "known bug" false pop end FreelyTests.«Σ_test_and_log_examples»

47. FreelyTests.Kit.Signature - the test DSL and its sum with logging🔗

This module contains:

  • A CPS signature Σ«Test» with two operations:

    • assert : String → Bool → (Unit → α) → Test α

    • xassert : String → Bool → (Unit → α) → Test α

  • SignatureLogAndTest := Log ⊕ᶠ Test (tests include logging).

  • deriveSend! manufactures façade constructors «assert», «xassert».

  • Convenience surface:

    • «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.

import Freely import FreelyLog set_option autoImplicit false open Freely open FreelyLog namespace FreelyTests /-- Minimal test signature: assertions + expected-failure assertions. -/ inductive Test (α : Type) : Type where | assert : (msg : String) (ok : Bool) (Unit α) Test α | xassert : (msg : String) (ok : Bool) (Unit α) Test α deriving Inhabited scoped notation "Σ«Test»" => Test instance : Functor Test where map g | .assert m ok continuation => .assert m ok (g continuation) | .xassert m ok continuation => .xassert m ok (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. -/ abbrev SignatureLogAndTest := Log ⊕ᶠ Test namespace SignatureLogAndTest deriveSend! (SignatureLogAndTest) FreelyTests.Test -- this gives us: -- «assert» -- «xassert» scoped notation "Σ«Log⊕Test»" => SignatureLogAndTest -- we add a few more convenience expressions to the language @[noinline] def expectTrue (msg : String) (b : Bool) : Σ«Log⊕Test» Unit := assert s!"{msg}" b @[noinline] def expectEq {α} [DecidableEq α] [ToString α] (msg : String) (x y : α) : Σ«Log⊕Test» Unit := assert s!"{msg} (got {x}, expected {y})" (decide (x = y)) -- and we reexport what we want from the Log language export FreelyLog.«Σ_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] def section {α : Type} (label : String) (k : Σ«Log⊕Test» α) : Σ«Log⊕Test» α := do push label let a k pop pure a end SignatureLogAndTest end FreelyTests

48. FreelyTests.Kit.State — run report and logging integration🔗

Report summarizes a test run:

  • counters: total, failed, xfails, ups (unexpected passes),

  • embedded LoggingState (via HasLoggingState instance).

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.

import FreelyLog set_option autoImplicit false open FreelyLog namespace FreelyTests /-- Test run summary + accumulated log state. -/ structure Report where total : Nat := 0 failed : Nat := 0 xfails : Nat := 0 ups : Nat := 0 logS : LoggingState := {} deriving Repr, Inhabited, DecidableEq instance : HasLoggingState Report where getLog r := r.logS setLog ls r := { r with logS := ls } @[inline] def testsOutput (r : Report) : String := let results := s!"tests: {r.total}, failed: {r.failed}, xfail: {r.xfails}, upass: {r.ups}" (testLogs r.logS) ++ "\n" ++ results where testLogs (s : LoggingState) : String := String.intercalate "\n" s.notes.toList end FreelyTests

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.

import Freely import FreelyLog import FreelyTests.Kit.State import FreelyTests.Kit.Handler import FreelyTests.Kit.Signature open Freely open Freely.DeriveTrace open FreelyLog open FreelyTests namespace FreelyTests deriveTrace! FreelyLog.Log deriveTrace! FreelyTests.Test def θ_log_traced : FreelyLog.Log ⟹ᶠ TestM := FreelyLog.tracedToLog («Σ» := FreelyLog.Log) (m := TestM) (σ := Report) θ_log def θ_test_traced : FreelyTests.Test ⟹ᶠ TestM := FreelyLog.tracedToLog («Σ» := FreelyTests.Test) (m := TestM) (σ := Report) θ_test -- requires `[TraceF Σ«Log»]` and `[TraceF FreelyTests.Test]` from `deriveTrace! def θ_log_and_test_traced : SignatureLogAndTest ⟹ᶠ TestM := Freely.copair θ_log_traced θ_test_traced end FreelyTests

․ Local Development🔗

⁚ Repository Structure

.
                                                         ⁞ README
├── README.md
                                                         ⁞ LOCAL DEVELOPMENT SCRIPTS
├── build.sh
├── docs.sh
                                                         ⁞ «freely» LIBRARY CONFIGURATION
├── lake-manifest.json
├── lakefile.toml
├── lean-toolchain
                                                         ⁞ «freely» SOURCE CODE
├── src
│   ├── Freely
│   │   ├── Algebra
│   │   │   └── ZeroPlus.lean
│   │   ├── Handler
│   │   │   ├── Algebra.lean
│   │   │   ├── Core.lean
│   │   │   ├── Derive
│   │   │   │   └── Trace.lean
│   │   │   └── Interpret.lean
│   │   ├── Program
│   │   │   └── Monad
│   │   │       ├── Derive
│   │   │       │   └── Member.lean
│   │   │       ├── Freer.lean
│   │   │       └── Member.lean
│   │   └── Signature
│   │       ├── Coyoneda.lean
│   │       └── Sum.lean
│   └── Freely.lean
                                                         ⁞ «freely-docs» SOURCE CODE
├── freely-docs
│   ├── freely-docs.code-workspace
│   ├── lake-manifest.json
│   ├── lakefile.toml
│   ├── lean-toolchain
│   └── src
│       ├── FreelyManual
│       │   ├── Citation.lean
│       │   ├── Freely
│       │   │   ├── Algebra
│       │   │   │   └── ZeroPlus.lean
│       │   │   ├── Handler
│       │   │   │   ├── Algebra.lean
│       │   │   │   ├── Core.lean
│       │   │   │   └── Interpret.lean
│       │   │   ├── Program
│       │   │   │   └── Monad
│       │   │   │       ├── Derive
│       │   │   │       │   └── Member.lean
│       │   │   │       ├── Freer.lean
│       │   │   │       └── Member.lean
│       │   │   └── Signature
│       │   │       ├── Coyoneda.lean
│       │   │       └── Sum.lean
│       │   ├── FreelyCategorically.lean
│       │   ├── FreelyCategoricallyRestated.lean
│       │   ├── FreelyDocs.lean
│       │   ├── FreelyLog
│       │   │   ├── Core.lean
│       │   │   ├── Handler.lean
│       │   │   ├── Interpret.lean
│       │   │   ├── Monad.lean
│       │   │   ├── Program.lean
│       │   │   ├── Signature.lean
│       │   │   ├── Trace.lean
│       │   │   └── Util.lean
│       │   ├── FreelyLog.lean
│       │   ├── FreelyTests
│       │   │   ├── Core.lean
│       │   │   ├── Fixtures
│       │   │   │   ├── Handlers
│       │   │   │   │   ├── Tick.lean
│       │   │   │   │   └── Tock.lean
│       │   │   │   ├── Signature
│       │   │   │   │   ├── Tick.lean
│       │   │   │   │   └── Tock.lean
│       │   │   │   ├── Tick.lean
│       │   │   │   ├── TickTock.lean
│       │   │   │   └── Tock.lean
│       │   │   ├── Freely
│       │   │   │   ├── Calc.lean
│       │   │   │   ├── Coyoneda.lean
│       │   │   │   ├── HandlerPlus.lean
│       │   │   │   ├── Handlers.lean
│       │   │   │   ├── Interpretation.lean
│       │   │   │   ├── MaskRouting.lean
│       │   │   │   ├── MonadLaws.lean
│       │   │   │   ├── Naturality.lean
│       │   │   │   └── SumHandles.lean
│       │   │   ├── Kit
│       │   │   │   ├── Handler.lean
│       │   │   │   ├── Interpret.lean
│       │   │   │   ├── Monad.lean
│       │   │   │   ├── Program.lean
│       │   │   │   ├── Signature.lean
│       │   │   │   ├── State.lean
│       │   │   │   └── Trace.lean
│       │   │   └── Suites
│       │   │       └── Freely.lean
│       │   ├── IntroducingFreely.lean
│       │   ├── License.lean
│       │   ├── Notice.lean
│       │   └── PriorArt.lean
│       ├── FreelyManual.lean
│       ├── Main.lean
│       └── static
│           ├── style copy.css
│           └── style.css
                                                         ⁞ `freely-examples` SOURCE CODE
├── freely-examples
│   ├── freely-examples.code-workspace
│   ├── lake-manifest.json
│   ├── lakefile.toml
│   ├── lean-toolchain
│   └── src
│       ├── FreelyExamples
│       │   └── Finance
│       │       ├── Demo.lean
│       │       ├── Handlers.lean
│       │       ├── Mask.lean
│       │       ├── Programs.lean
│       │       ├── Shreve1.lean
│       │       └── Signature.lean
│       └── FreelyExamples.lean
                                                         ⁞ «freely-log» SOURCE CODE
├── freely-log
│   ├── freely-log.code-workspace
│   ├── lake-manifest.json
│   ├── lakefile.toml
│   ├── lean-toolchain
│   └── src
│       ├── FreelyLog
│       │   ├── Core.lean
│       │   ├── Examples
│       │   │   └── Main.lean
│       │   ├── Handler.lean
│       │   ├── Interpret.lean
│       │   ├── Monad.lean
│       │   ├── Program.lean
│       │   ├── Signature.lean
│       │   ├── Trace.lean
│       │   └── Util.lean
│       └── FreelyLog.lean
                                                         ⁞ «freely-tests» SOURCE CODE
├── freely-tests
│   ├── freely-tests.code-workspace
│   ├── lake-manifest.json
│   ├── lakefile.toml
│   ├── lean-toolchain
│   └── src
│       ├── FreelyTests
│       │   ├── Fixtures
│       │   │   ├── Handlers
│       │   │   │   ├── Tick.lean
│       │   │   │   └── Tock.lean
│       │   │   ├── Signature
│       │   │   │   ├── Tick.lean
│       │   │   │   └── Tock.lean
│       │   │   ├── Tick.lean
│       │   │   ├── TickTock.lean
│       │   │   └── Tock.lean
│       │   ├── Freely
│       │   │   ├── Calc.lean
│       │   │   ├── Coyoneda.lean
│       │   │   ├── HandlerPlus.lean
│       │   │   ├── Handlers.lean
│       │   │   ├── Interpretation.lean
│       │   │   ├── MaskRouting.lean
│       │   │   ├── MonadLaws.lean
│       │   │   ├── Naturality.lean
│       │   │   └── SumHandles.lean
│       │   ├── Kit
│       │   │   ├── Handler.lean
│       │   │   ├── Interpret.lean
│       │   │   ├── Monad.lean
│       │   │   ├── Program.lean
│       │   │   ├── Signature.lean
│       │   │   ├── State.lean
│       │   │   └── Trace.lean
│       │   ├── Main.lean
│       │   └── Suites
│       │       └── Freely.lean
│       └── FreelyTests.lean
                                                         ⁞ LICENSING AND CITATION
├── CITATION.cff
├── CONTRIBUTING.md
├── LICENSE
├── NOTICE

Local build scripts

To compile everything except the docs site (runs tests):

➜ ./build.sh

To compile everything including the docs site (running tests), and also serving the docs site (using python3):

➜ ./docs.sh

Tooling you might need

python3

If you need python3, you can install it with uv.

Get uv:

curl -LsSf https://astral.sh/uv/install.sh | sh

Get the right version of python in a venv and sync uv

uv venv
uv sync  # creates uv.lock and installs exact versions

lean4code IDE (clone of vscode w/ lean specific bells and whistles)

The lean4code IDE requires npm to install. If you need node/npm you can install nvm and manage with that:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
nvm install 22
nvm use 22

I work with the following versions

➜ nvm -v
0.40.3
➜ node -v
v24.5.0
➜ npm -v
11.5.1

Get lean4code:

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.):

alias lean4code="open <path to Lean4Code.app>"

Now you can invoke lean4code from the terminal:

➜ lean4code

․ License, citation, and use of «freely»🔗

50. License (Apache 2.0)

This work is licensed under Apache License, Version 2.0

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.

53. ․ Resources🔗

53.1. ⁚ Lean 4 resources🔗

53.2. ⁚ Category Theory resources (unrelated to free(r) monads)🔗

53.2.1. ⁖ Sites

53.2.2. ⁖ Category Theory Books

53.2.3. ⁖ String Diagrams Books (Category Theory meets Computation)

53.2.4. ⁖ Topos Theory / Sheaf Theory Books

I would be remiss if I didn't include Daniel Rosiak's thesis "Towards a Classification of Continuity and On the Emergence of Generality", for the philosophically inclined.

․ Author (Abraham Howland)🔗

Abraham Howland

⁚ Résumé🔗

⁚ Contact🔗