Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
ccc42e5
feat(schemes): typed pattern-functor recursion schemes (cataF/anaF/hy…
kryptt Jun 9, 2026
357804c
docs(plans): mark typed-recursion-schemes plan completed
kryptt Jun 9, 2026
6d2f6f8
perf(schemes): JMH bench for typed cataF/anaF/hyloF vs droste basic (U6)
kryptt Jun 9, 2026
0b7aebb
perf(schemes): replace typed Eval driver with the foldLayered heap ma…
kryptt Jun 9, 2026
af5a91f
docs(schemes): lens-composition example for the typed schemes
kryptt Jun 9, 2026
08df603
fix(schemes): post-rebase reconciliation + BiAffine zoo plan
kryptt Jun 11, 2026
63cf7f4
fix(docs): schemes.md DirectGetter→Getter (post-rebase rename sweep)
kryptt Jun 11, 2026
bdfd209
feat(core): BiAffine carrier + Graft capability — the decoration foun…
kryptt Jun 11, 2026
1e06c4a
feat(schemes): Attr/Coattr — histo/futu decoration data
kryptt Jun 11, 2026
ff2bdc2
feat(schemes): Decor family — decorations as BiAffine optics, cataF/a…
kryptt Jun 11, 2026
9002b98
feat(schemes): the typed zoo — paraF (no re-embed), apoF (O(1) graft)…
kryptt Jun 11, 2026
bfad423
feat(schemes): CataF/AnaF citizens + fused cross — deforestation as c…
kryptt Jun 11, 2026
3df7cfd
feat(schemes): M-generic drivers — cataFM/anaFM/hyloFM on the tailRec…
kryptt Jun 11, 2026
bee9988
perf(schemes): zoo + fusion + M-path + generic-route bench rows
kryptt Jun 11, 2026
beeb8c0
docs(schemes): the zoo — symmetry table, fusion-as-cross, zygo-as-Dec…
kryptt Jun 11, 2026
21c985d
docs(plans): mark BiAffine zoo plan implemented (bench numbers + revi…
kryptt Jun 11, 2026
e177f68
perf(schemes): CI-driven allocation wins — M-machine −49% B/op, fused…
kryptt Jun 12, 2026
c37a8f9
fix(schemes): review fixes — fresh-state-per-force M-machine, single-…
kryptt Jun 12, 2026
bc6fe7c
refactor(schemes)!: drop the untyped PSVec cata/ana/hylo — the typed …
kryptt Jun 12, 2026
b6bea24
refactor(schemes)!: strip the F suffix — the typed schemes own the ba…
kryptt Jun 12, 2026
9af72cb
refactor(schemes): Decor → Gather/Scatter; para/apo decorations demot…
kryptt Jun 12, 2026
1ed3a82
refactor(schemes): zoo package + Machines split + concurrency proof (…
kryptt Jun 12, 2026
73960fb
refactor(schemes): Gather/Scatter as classes, dispatch-free drivers, …
kryptt Jun 12, 2026
8977550
docs(brainstorms): X-is-the-decoration — the optics⇄schemes connectio…
kryptt Jun 12, 2026
fa8651b
ci: compile test sources before running tests — fixes roving Kindling…
kryptt Jun 12, 2026
9cb8d51
refactor(schemes): union-typed slots, one shared heap walk, named FLa…
kryptt Jun 12, 2026
b22cbd0
docs(benchmarks): regression check across the review-refactor window …
kryptt Jun 12, 2026
ac476f4
refactor(schemes): centralise Machines' unchecked narrowings into 5 n…
kryptt Jun 13, 2026
ff6d8b0
refactor(schemes)!: ana/apo/futu forward Getter; drop cross + clone c…
kryptt Jun 13, 2026
0456c31
style(benchmarks): apply scalafmt to drifted benchmark sources
kryptt Jun 13, 2026
2343f94
fix(schemes)!: ana is a Review, not a Getter — restore the cata/ana d…
kryptt Jun 13, 2026
46c6d0f
spike(schemes): X-indexed fusion prototype — ana.cross(cata) at hylo …
kryptt Jun 13, 2026
0f4d569
docs(schemes): clean up stale names from the rename/flip experiments
kryptt Jun 13, 2026
383d04e
refactor(schemes)!: recast schemes as existential-indexed optics; hyl…
kryptt Jun 14, 2026
93c825b
feat(schemes): metamorphism (meta) + metaChrono — the non-fusing fold…
kryptt Jun 14, 2026
63fcf37
test(schemes): F=:=G still does not fuse meta — the scalar neck is th…
kryptt Jun 14, 2026
31de406
feat(schemes): para/apo, elgot/coelgot, dyna/codyna — fill the zoo
kryptt Jun 14, 2026
c36efaf
refactor(schemes): drop Scheme carrier for Direct; one class per scheme
kryptt Jun 14, 2026
d252951
refactor(schemes): dedup audit — centralize carrier, extract expand/d…
kryptt Jun 14, 2026
1876c29
feat(schemes): zygo/mutu/cozygo/comutu + prepro/postpro — close the zoo
kryptt Jun 14, 2026
6ab378a
feat(schemes): monadic *M family — cataM/anaM/hyloM & co on the laten…
kryptt Jun 14, 2026
c6cfb8a
feat(core): move Basis into core, derive Plated from it — the schemes…
kryptt Jun 14, 2026
c2d3180
feat(schemes): fLayer rides MultiFocus[F] — a typed single-layer self…
kryptt Jun 14, 2026
6fec9a4
feat(schemes): paraLens — the paramorphism as a lawful, composable Lens
kryptt Jun 14, 2026
148e42e
feat(core): BiAffine.assoc — ship the deferred composition-matrix row
kryptt Jun 14, 2026
c4ecb6a
feat(core): BiAffine cross-carrier bridges — complete the matrix row
kryptt Jun 14, 2026
e87e5f3
feat(schemes): re-carrier apo onto BiAffine via apoScatter
kryptt Jun 14, 2026
e98731c
refactor(schemes): bring the M-family + unfold drive to the pure side…
kryptt Jun 14, 2026
d2510e0
build: drop the schemes-laws module — its subject (the untyped PSVec …
kryptt Sep 4, 2026
016b137
fix(core): drop the unused Eval import from Plated — rebase-merge art…
kryptt Sep 4, 2026
2d51b04
docs: typed-schemes bibliography + merge-readiness cleanup plan
kryptt Sep 4, 2026
cdd5175
chore(schemes): resolve Scaladoc link warnings + drop empty schemes-l…
kryptt Sep 4, 2026
190804e
fix(docs): rewrite site/docs/schemes.md against the shipped schemes API
kryptt Sep 4, 2026
93f7b3e
docs: CHANGELOG entries for the schemes surface (C4) + plan-doc bibli…
kryptt Sep 4, 2026
24a8839
feat(core): C8 — break the Getter.andThen 3-way tie for Direct-carrie…
kryptt Sep 4, 2026
b4a4454
perf(schemes): restore para's positional slot-pairing (C7 regression …
kryptt Sep 4, 2026
75d44a4
fix(schemes): adapt to the rebased base — Affine.Miss[A] arity, widen…
kryptt Sep 4, 2026
2d85405
style(benchmarks): scalafmt the re-pinned sources
kryptt Sep 4, 2026
40330a4
fix(schemes): para rides a typed paired engine; docs re-pinned withou…
kryptt Sep 30, 2026
8236279
test(schemes): pin para's route - one peel per node (the regression w…
kryptt Sep 30, 2026
d703be5
style(schemes): scalafmt the para-engine sources after the rebase
kryptt Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ jobs:
- name: Check scalafix
run: sbt '++ ${{ matrix.scala }}' 'scalafixAll --check'

- name: Compile tests (macro expansion before test-run contention)
run: sbt '++ ${{ matrix.scala }}' Test/compile

- name: Check that workflows are up to date
run: sbt githubWorkflowCheck

Expand All @@ -135,11 +138,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
run: mkdir -p jsoniter/target benchmarks/target kyo/target target circe/target zio/target unidocs/target avro/target laws/target tests/target generics/target schemes-laws/target schemes/target core/target project/target
run: mkdir -p jsoniter/target benchmarks/target kyo/target target circe/target zio/target unidocs/target avro/target laws/target tests/target generics/target schemes/target core/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
run: tar cf targets.tar jsoniter/target benchmarks/target kyo/target target circe/target zio/target unidocs/target avro/target laws/target tests/target generics/target schemes-laws/target schemes/target core/target project/target
run: tar cf targets.tar jsoniter/target benchmarks/target kyo/target target circe/target zio/target unidocs/target avro/target laws/target tests/target generics/target schemes/target core/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
Expand Down
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,53 @@ grep-verified and the one perf-relevant cut B/op-verified:

### Added

- **`cats-eo-schemes` — typed recursion schemes as composable optics.** A new
module whose citizens are optics over a user-supplied **pattern functor**
`F[_]` (+ `Traverse[F]`) and a hand-written `Basis` (`Project[F, S]` /
`Embed[F, S]`): `cata` (Getter-shaped fold), `ana` (Review-shaped unfold),
and the **fused** `hylo` (builds no intermediate `S`), plus the materialising
`ana.cross(cata)` spelling (the hylo law pins the two as equal for a pure
algebra). The **decoration zoo** refines each tower rung by its existential
index — `para` (subterm-retaining, product), `apo` (O(1) subtree graft, sum),
`histo` (course-of-value over the cofree `Attr`), `futu` (multi-layer unfold
over the free `Coattr`), plus `zygo` / `mutu` / `cozygo` / `comutu` between
the towers, the fused `dyna` / `codyna` / `chrono` / `elgot` / `coelgot`
refolds, `meta` / `metaChrono` (the non-fusing fold→unfold seam), and
`prepro` / `postpro` (the natural-transformation axis). The **effectful
`*M` family** (`cataM` / `paraM` / `histoM` / `anaM` / `apoM` / `futuM` /
`hyloM` / `chronoM`) runs the same machine lifted through
`Monad[M].tailRecM` (single-pass, linear `M` contract). `paraLens` promotes
the paramorphism to a lawful `Lens` (caller-supplied coherent put). All
schemes run on one stack-safe `< 512`-on-stack / heap-`ArrayDeque` engine,
tested to 10⁶ depth; the typed path replaces the earlier untyped
`Plated`/`PSVec`-driven schemes (removed — the erased positional indexing
made algebra arity slips a runtime error).

- **`Affine` wears the build seam — the schemes' decoration carrier, no new carrier.**
The decoration machinery reuses `data.Affine` with its arms read on the build
seam: `Hit(context, focus)` keeps going, `Miss(payload)` means "this slot is
already finished — do not call the coalgebra" (apo grafts by reference, futu
unrolls a prebuilt layer). New in core: the `Graft[Affine]` build-channel
accessor (`done = Miss`, `step = Hit`) — the injection vocabulary the schemes'
build-side citizens construct and consume — plus the corresponding
graft-finality laws in `cats-eo-laws` (the finished arm is focus-free, inert
under `map`, and folds empty). Decoration composition rides Affine's own
composition row (`Affine.assoc` same-carrier `andThen`) and its cross-carrier
bridges from `Tuple2` (Lens) and `Either` (Prism), so
`lens.andThen(apoScatter)`-style compositions resolve. `Schemes.apoScatter`
exposes the `Left(s) → Miss(s)` graft channel as a composable scatter optic.
(An earlier draft shipped a separate `BiAffine` carrier with identical shape;
it was dropped pre-merge — the arms are isomorphic and the duplication bought
nothing. The `BiAffine` name is left free for a genuinely two-sided-failure
carrier if one is ever needed.)

- **`Basis` in core; `Plated` derives from it.** `optics.Basis`
(`Project[F, S]` / `Embed[F, S]`) — the pattern-functor correspondence the
schemes drive — moves into `cats-eo-core`, and `Plated.fromBasis` derives a
`Plated[S]` from it, the schemes↔`Plated` bridge (`PlatedBridgeSpec` pins
`embed ∘ project` coherence and universe/transform agreement with `cata`).


- **`Getter`s now compose with `Getter`s via `andThen`.** `g1.andThen(g2)` reads
`s => g2.get(g1.get(s))` and yields a `Getter`, matching how `Iso` / `Lens`
compose through their fused subclasses. `Getter.apply` now returns a concrete
Expand Down
5 changes: 2 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ Test-only: `org.typelevel:discipline-specs2_3:2.0.0`.
| `tests` | `tests/` | — (not published) | Law-based and behavioural test suites |
| `generics` | `generics/` | `cats-eo-generics` | Auto-derivation of Lens/Prism via Scala 3 quoted macros |
| `schemes` | `schemes/` | `cats-eo-schemes` | Recursion schemes (cata/ana/hylo) as composable optics |
| `schemesLaws` | `schemes-laws/` | `cats-eo-schemes-laws` | Laws for the recursion schemes (hylo fusion so far; more expected) — separate from `laws` because they quantify over `schemes` types |
| `circe` | `circe/` | `cats-eo-circe` | `Plated[Json]` and circe optic integration |
| `avro` | `avro/` | `cats-eo-avro` | Apache Avro optic integration; the `eo.avro.circe` sub-package is the structural Avro ↔ circe bridge (`AvroJson` + the `.json` / `.avro` cursor faces on `AvroPrism`/`JsonPrism`), `eo.avro.jsoniter` the AST-free Avro ↔ JSON-bytes twin (`AvroJsoniter` + the same faces on `JsoniterPrism`), and `eo.avro.vulcan` bridges `vulcan.Codec` → `AvroCodec` (`AvroVulcan`) — circe, cats-eo-circe, jsoniter-scala-core, cats-eo-jsoniter, and vulcan are `Optional` deps, callers add them themselves. NB avro depends on the circe/jsoniter MODULES (Optional); the reverse would be a project cycle, so the cross-format bridge specs live in `avro/src/test` |
| `jsoniter` | `jsoniter/` | `cats-eo-jsoniter` | jsoniter-scala optic integration |
Expand All @@ -30,7 +29,7 @@ Test-only: `org.typelevel:discipline-specs2_3:2.0.0`.
| `benchmarks` | `benchmarks/` | — (not published) | JMH benchmarks vs Monocle (not part of root aggregate) |

The root project aggregates `core`, `laws`, `tests`, `generics`, `schemes`,
`schemesLaws`, `circe`, `avro`, `jsoniter`, `zio`, and `kyo`. `sbt compile` and `sbt test`
`circe`, `avro`, `jsoniter`, `zio`, and `kyo`. `sbt compile` and `sbt test`
cover those; benchmarks must be invoked explicitly (see below).

## Toolchain
Expand Down Expand Up @@ -140,7 +139,7 @@ Key facts, all the hard-won kind:
module-scoped task form reads `loadedTestFrameworks` from the
aggregating root project (no test deps), so specs2 is invisible and
*every* mutant comes back `NoCoverage`. The `mutationAll` alias uses the
project-switch form across core, laws, generics, schemes, schemesLaws,
project-switch form across core, laws, generics, schemes,
circe, avro, jsoniter.
- **It's a report, not a gate** (`strykerThresholdsBreak := 0`): a low
score never fails the build.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package dev.constructive.eo
package bench

import org.openjdk.jmh.annotations.*
import java.util.concurrent.TimeUnit

import higherkindness.droste.data.Fix
import higherkindness.droste.scheme

import dev.constructive.eo.bench.fixture.*
import dev.constructive.eo.bench.fixture.PlatedTrees.eoBin // given Plated[Bin]
import dev.constructive.eo.bench.fixture.SchemesFixtures.given
import dev.constructive.eo.schemes.Schemes
import higherkindness.droste.data.Fix
import higherkindness.droste.scheme
import java.util.concurrent.TimeUnit
import org.openjdk.jmh.annotations.*

/** Recursion schemes — `cata` / `ana` / `hylo` — three ways, on the same workload:
/** Recursion schemes — `cata` / `ana` / `hylo` — four ways, on the same workload:
*
* - **eo** — schemes as optics over the *native* `Bin` (`cata` driven by `Plated[Bin]`, `ana` a
* `Review`, fused `hylo` a `Getter`), all on one stack-safe heap machine.
* - **droste** — the pattern-functor + `Fix[BinF]` encoding (`scheme.cata/ana/hylo`).
* - **eoF** — the typed pattern-functor path (`cata`/`ana`/`hylo` over `BinF` via a `Basis` +
* `Traverse[BinF]`) on the stack-safe `foldLayered` heap machine. (The untyped `PSVec` path
* was removed once the typed path subsumed it.)
* - **droste** — the pattern-functor + `Fix[BinF]` encoding (`scheme.cata/ana/hylo`). NB
* droste's *basic* schemes are stack-*unsafe* (naive recursion); `eoF` delivers the
* stack-safety they lack, so the comparison is not apples-to-apples.
* - **hand** — plain recursion on `Bin`, the baseline you'd write without either library.
*
* Workload is a perfect binary tree of `2^Depth` `Leaf(1)`s (Depth = 12 ⇒ 4096 leaves, 8191
Expand All @@ -40,9 +44,10 @@ class SchemesBench extends JmhDefaults:
val fixTree: Fix[BinF] = balancedFix(Depth)

// Prebuilt scheme optics / functions (construction not measured).
val eoCataG = Schemes.cata(eoSum) // DirectGetter[Bin, Int]
val eoHyloG = Schemes.hylo(eoExpand, eoHyloAlg) // DirectGetter[Int, Int]
val eoAnaR = Schemes.ana(eoAnaCoalg) // Review[Bin, Int]
// typed pattern-functor path (Eval trampoline over Traverse[BinF])
val eoCataG = Schemes.cata(eoTypedSum) // Getter[Bin, Int]
val eoHyloG = Schemes.hylo(eoTypedCoalg, eoTypedHyloAlg) // Getter[Int, Int]
val eoAnaR = Schemes.ana[BinF, Int, Bin](eoTypedCoalg) // Review[Bin, Int]

val drosteCataF: Fix[BinF] => Int = scheme.cata(drosteSum)
val drosteHyloF: Int => Int = scheme.hylo(drosteSum, drosteBuild)
Expand All @@ -62,3 +67,61 @@ class SchemesBench extends JmhDefaults:
@Benchmark def eoAna: Bin = eoAnaR.reverseGet(Depth)
@Benchmark def drosteAna: Fix[BinF] = drosteAnaF(Depth)
@Benchmark def handAna: Bin = handBuild(Depth)

// ----- the zoo: para / apo / histo / futu (eo native routes vs droste.zoo) --

val eoParaG = Schemes.para[BinF, Bin, Int](eoParaAlg)
val drosteParaFn: Fix[BinF] => Int = scheme.zoo.para(drosteParaAlg)
val eoApoR = Schemes.apo[BinF, Int, Bin](eoApoCoalg)
val drosteApoFn: Int => Fix[BinF] = scheme.zoo.apo(drosteApoCoalg)
val eoHistoG = Schemes.histo[BinF, Bin, Int](eoHistoAlg)
val drosteHistoFn: Fix[BinF] => Int = scheme.zoo.histo(drosteHistoAlg)
val eoFutuR = Schemes.futu[BinF, Int, Bin](eoFutuCoalg)
val drosteFutuFn: Int => Fix[BinF] = scheme.zoo.futu(drosteFutuCoalg)

@Benchmark def eoPara: Int = eoParaG.get(eoTree)
@Benchmark def drostePara: Int = drosteParaFn(fixTree)
@Benchmark def eoApo: Bin = eoApoR.reverseGet(Depth)
@Benchmark def drosteApo: Fix[BinF] = drosteApoFn(Depth)
@Benchmark def eoHisto: Int = eoHistoG.get(eoTree)
@Benchmark def drosteHisto: Int = drosteHistoFn(fixTree)
@Benchmark def eoFutu: Bin = eoFutuR.reverseGet(Depth)
@Benchmark def drosteFutu: Fix[BinF] = drosteFutuFn(Depth)

// ----- apo with ONE BIG GRAFT. VERIFIED (the D6 check): droste's zoo.apo
// ALSO grafts O(1) here — its R is the fixed point, so Left(fix) embeds by
// reference. The honest claim is therefore PARITY on the native routes (both
// ~ns-flat regardless of graft size), with eo adding the law-shaped eq
// guarantee; the O(graft) re-walk contrast applies to the GENERIC distApo
// route (distApo, a law fixture only), not to droste.zoo.apo.

val eoApoGraftR = Schemes.apo[BinF, Int, Bin] { d =>
if d == 0 then BinF.NodeF(Left(eoTree), Right(-1)) else BinF.LeafF(1)
}

val drosteApoGraftFn: Int => Fix[BinF] = scheme
.zoo
.apo(
higherkindness.droste.RCoalgebra { (d: Int) =>
if d == 0 then BinF.NodeF(Left(fixTree), Right(-1)) else BinF.LeafF(1)
}
)

@Benchmark def eoApoGraft: Bin = eoApoGraftR.reverseGet(0)
@Benchmark def drosteApoGraft: Fix[BinF] = drosteApoGraftFn(0)

// ----- materializing refold: cross-spelling vs manual (both build the Bin) --
// `ana.cross(cata)` is the build⇄read seam — the materialising hylo (builds the
// Bin, then folds), so the two spellings allocate identically. The *fused*
// (no-intermediate-Bin) contrast is `eoHylo` above (~half the B/op).

val eoRefoldCrossG = Schemes.ana[BinF, Int, Bin](eoTypedCoalg).cross(Schemes.cata(eoTypedSum))

@Benchmark def eoRefoldCross: Int = eoRefoldCrossG.get(Depth)
@Benchmark def eoRefoldManual: Int = eoCataG.get(eoAnaR.reverseGet(Depth))

// ----- the M path at Id: the tailRecM-lifted machine's per-event floor ------

val eoHyloMRunner = Schemes.hyloM[cats.Id, BinF, Int, Int](eoTypedCoalg, fa => eoTypedHyloAlg(fa))

@Benchmark def eoHyloM: Int = eoHyloMRunner.get(Depth)
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package dev.constructive.eo
package bench
package fixture

import cats.Functor
import dev.constructive.eo.data.PSVec
import dev.constructive.eo.schemes.Schemes
import cats.{Applicative, Eval, Traverse}
import higherkindness.droste.data.Fix
import higherkindness.droste.{Algebra, Coalgebra}

import dev.constructive.eo.schemes.Basis

/** Pattern functor for the native [[Bin]] tree (`Leaf(Int)` / `Node(Bin, Bin)`).
*
* droste requires this pattern-functor + `Fix` encoding to express recursion schemes; eo works on
Expand All @@ -21,12 +21,41 @@ enum BinF[+A]:

object SchemesFixtures:

given binFunctor: Functor[BinF] with
/** `Traverse[BinF]` — serves both droste (which needs only `Functor[BinF]`, obtained via the
* `Traverse <: Functor` subtype) and eo's typed schemes (which need the full `Traverse`). A
* single instance avoids an ambiguous `Functor[BinF]` summon. `foldRight` is `Eval`-based so the
* typed driver's trampoline stays lazy.
*/
given binTraverse: Traverse[BinF] with

def traverse[G[_]: Applicative, A, B](fa: BinF[A])(f: A => G[B]): G[BinF[B]] =
fa match
case BinF.LeafF(v) => Applicative[G].pure(BinF.LeafF(v))
case BinF.NodeF(l, r) => Applicative[G].map2(f(l), f(r))(BinF.NodeF(_, _))

def map[A, B](fa: BinF[A])(f: A => B): BinF[B] =
def foldLeft[A, B](fa: BinF[A], b: B)(f: (B, A) => B): B =
fa match
case BinF.LeafF(v) => BinF.LeafF(v)
case BinF.NodeF(l, r) => BinF.NodeF(f(l), f(r))
case BinF.LeafF(_) => b
case BinF.NodeF(l, r) => f(f(b, l), r)

def foldRight[A, B](fa: BinF[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
fa match
case BinF.LeafF(_) => lb
case BinF.NodeF(l, r) => f(l, Eval.defer(f(r, lb)))

/** `Basis[BinF, Bin]` — the `Project`/`Embed` correspondence between the native `Bin` and its
* pattern functor, for the typed `cata`/`ana` benches.
*/
given binBasis: Basis[BinF, Bin] = Basis(
{
case Bin.Leaf(v) => BinF.LeafF(v)
case Bin.Node(l, r) => BinF.NodeF(l, r)
},
{
case BinF.LeafF(v) => Bin.Leaf(v)
case BinF.NodeF(l, r) => Bin.Node(l, r)
},
)

// ----- droste algebra / coalgebra (over BinF, on Fix[BinF]) ----------------

Expand All @@ -40,25 +69,18 @@ object SchemesFixtures:
if d <= 0 then BinF.LeafF(1) else BinF.NodeF(d - 1, d - 1)
}

// ----- eo algebra / coalgebra (over native Bin via Plated) -----------------
// ----- eo TYPED algebras (over the pattern functor BinF via Basis/Traverse) ----------------

val eoSum: (Bin, PSVec[Int]) => Int = (node, kids) =>
node match
case Bin.Leaf(v) => v
case Bin.Node(_, _) => kids(0) + kids(1)
/** Typed cata algebra — the leaf-sum, pattern-matching `BinF`'s named constructors. */
val eoTypedSum: BinF[Int] => Int = { case BinF.LeafF(v) => v; case BinF.NodeF(l, r) => l + r }

/** Seed expansion shared by eo's hylo/ana: depth `d` ⇒ two child seeds `(d-1, d-1)`; leaf at
* `d <= 0`. `PSVec.of` builds the 2-vector directly (no `List` intermediate).
/** Typed coalgebra (the single fused `Seed => F[Seed]` shape) — builds the perfect binary tree.
*/
val eoExpand: Int => PSVec[Int] = d => if d <= 0 then PSVec.empty[Int] else PSVec.of(d - 1, d - 1)

/** Fused hylo algebra — folds to `Int` directly, never building a `Bin`. */
val eoHyloAlg: (Int, PSVec[Int]) => Int = (d, rs) => if d <= 0 then 1 else rs(0) + rs(1)
val eoTypedCoalg: Int => BinF[Int] = d =>
if d <= 0 then BinF.LeafF(1) else BinF.NodeF(d - 1, d - 1)

/** ana coalgebra (bundled) — each seed's child seeds + how to assemble a native `Bin`. */
val eoAnaCoalg: Schemes.Coalg[Int, Bin] = d =>
if d <= 0 then (PSVec.empty[Int], (_: PSVec[Bin]) => Bin.Leaf(1))
else (PSVec.of(d - 1, d - 1), (ks: PSVec[Bin]) => Bin.Node(ks(0), ks(1)))
/** Typed fused-hylo algebra — folds to `Int` directly, never building a `Bin`. */
val eoTypedHyloAlg: BinF[Int] => Int = { case BinF.LeafF(_) => 1; case BinF.NodeF(l, r) => l + r }

// ----- hand-wired recursion (the baseline you'd write without either lib) --

Expand All @@ -80,3 +102,48 @@ object SchemesFixtures:
def balancedFix(d: Int): Fix[BinF] =
if d <= 0 then Fix(BinF.LeafF(1))
else Fix(BinF.NodeF(balancedFix(d - 1), balancedFix(d - 1)))

// ----- zoo fixtures (para / apo / histo / futu — eo vs droste) -------------

import higherkindness.droste.{CVAlgebra, CVCoalgebra, RAlgebra, RCoalgebra}
import higherkindness.droste.data.{Attr => DAttr, Coattr => DCoattr}
import dev.constructive.eo.schemes.zoo.{Attr => EoAttr, Coattr => EoCoattr}

// para: the same leaf-sum with subterms IGNORED — measures pure decoration
// overhead (eo pairs subterms from the walked nodes; droste re-embeds each).
val eoParaAlg: BinF[(Bin, Int)] => Int = {
case BinF.LeafF(v) => v; case BinF.NodeF((_, l), (_, r)) => l + r
}

val drosteParaAlg: RAlgebra[Fix[BinF], BinF, Int] = RAlgebra {
case BinF.LeafF(v) => v
case BinF.NodeF((_, l), (_, r)) => l + r
}

// apo, never grafting: the build-side decoration overhead row.
val eoApoCoalg: Int => BinF[Either[Bin, Int]] = d =>
if d <= 0 then BinF.LeafF(1) else BinF.NodeF(Right(d - 1), Right(d - 1))

val drosteApoCoalg: RCoalgebra[Fix[BinF], BinF, Int] = RCoalgebra { d =>
if d <= 0 then BinF.LeafF(1) else BinF.NodeF(Right(d - 1), Right(d - 1))
}

// histo, heads only: the course-of-value bookkeeping cost.
val eoHistoAlg: BinF[EoAttr[BinF, Int]] => Int = {
case BinF.LeafF(v) => v; case BinF.NodeF(l, r) => l.head + r.head
}

val drosteHistoAlg: CVAlgebra[BinF, Int] = CVAlgebra {
case BinF.LeafF(v) => v
case BinF.NodeF(l, r) => DAttr.un(l)._1 + DAttr.un(r)._1
}

// futu, single layer per step: the free-wrapper cost.
val eoFutuCoalg: Int => BinF[EoCoattr[BinF, Int]] = d =>
if d <= 0 then BinF.LeafF(1)
else BinF.NodeF(EoCoattr.Pure(d - 1), EoCoattr.Pure(d - 1))

val drosteFutuCoalg: CVCoalgebra[BinF, Int] = CVCoalgebra { d =>
if d <= 0 then BinF.LeafF(1)
else BinF.NodeF(DCoattr.pure(d - 1), DCoattr.pure(d - 1))
}
Loading
Loading