Inspired by Rúnar Bjarnason, Oleg Kiselyov and Robert Atkey.
http://blog.higher-order.com/assets/trampolines.pdf "Stackless Scala With Free Monads" Rúnar Óli Bjarnason
https://okmij.org/ftp/Haskell/extensible/more.pdf "Freer Monads, More Extensible Effects" Oleg Kiselyov
https://bentnib.org/paramnotions-jfp.html "Parameterised notions of computation" Robert Atkey
Where every other idea here comes from, with the papers: the theory of Okay.
Zero dependencies. One source for JVM (JDK 21+, Loom), Scala.js and Scala Native — each platform contributes evidence (can it park? what is its timer? what schedules?), not API: the same Await-based test suite runs on a JVM, under Node and as a linked native binary.
Cont[A, S, R](Cont.scala) — the parameterised continuation monad (answer-type modification, shift/reset), defunctionalized like Free, so running a flatMap chain is stack-safe.Control[M[_, _, _]](Cont.scala) — final tagless interface of delimited control; instances:Cont(stack-safe data) andFunc(the function encoding, the reference).Effects[M[_[+_], _]](Effects.scala) — final tagless interface of extensible effects, founded on the continuation paramonad: a handler isF !> S = F ==> ([X] =>> X /> S), an interpretation of the operations in Cont, and the meaning of a computation is itsfoldCont;runWithandhandlederive from it. Instances:Free(initial, defunctionalized) andEff(final, Church). Choosing: the tree is for tools (stepping, staged relay, stack safety on any bind shape), the function is for speed (fused build-and-run pipelines), and the interface is for not choosing too early —fromFreeandreifymove programs between the encodings.!.relay(Effects.scala) — tail-resumptive handling: the answer-polymorphic handler must resume exactly once, which keeps the loop tail-recursive.Effects.handle— general handlers (abort, forwarding), via foldCont.
Reader— the environment, handled at relay speed (Reader.scala).Writer— telling IS streaming: a one-constructor GADT (Say(w): Writer[W, Unit]— a tell answers NOTHING, and matching the constructor recovers that, so nothing casts), the element type separate from the answer (A ! Writer % Wcomputes A telling W); run/fold into any Fold algebra, uncons asEither[A, (W, rest)];Writer.ofturns any stream back into the program shape,Writer.mapre-tells at another type (Writer.scala; the five encodings tried before this one: docs/existentials.md).State— get/set with a bespoke tail-recursive handler;PState— type-changing (typestate) state on the paramonad (State.scala).Throws— typed errors: abort, runEither, thethrowsunion (Throws.scala).Choice— nondeterminism with a genuinely multi-shot handler; the canonical MonadPlus (Choice.scala).Logic— fair backtracking search on top of it: interleave, once, ifte (Logic.scala, specs/backtracking.md).Async— cross-platform:Run(a possibly blocking thunk — blocking is a JVM/Native ability that parks a virtual thread) andAwait(the universal callback form: an error channel in, a canceller out). Blocking isCanBlockevidence — absent on JS, whererunAsyncdrives the same programs through the event loop and a blocking join is a compile error.spawn/par/race/timeout/sleepare cross-platform;Fiberis onComplete/cancel/joinAsync everywhere, parking join under the evidence;Schedulertakes the program (Loom / the event loop / one OS thread per fiber) (Async.scala + Platform.scala per platform).Resource— the region: acquires release at the end of the scope in reverse order, surviving handled aborts and mid-step exceptions;bracketover any Handler-able row (Resource.scala).
A stream is codata: one observation, uncons — effectful, in
Stream[S[_], F[+_]] (F = Pure for pure, Async for awaited
elements). LazyList is the final coalgebra every stream unfolds into.
Consumption modes, slow-to-fast on a map/filter/take(1000)/sum
pipeline (JMH, us/op; plain Iterator floor = 14.1):
| mode | us | note |
|---|---|---|
.toLazyList + combinators |
143 | memoized, re-observable |
.iterator |
53 | linear, fused, consume-once |
Chunks + .elements |
23.6 | chunked source |
Chunks.map/filter/take |
16.9 | chunk-in, chunk-out array passes |
Staged (inline whole-stage) |
1.6 | one fused while-loop; same-run Iterator = 19.3 |
(kyo 239, ZIO 692, fs2 1410 on the same pipeline. Staged is the
compile-time end of the choice rule: the Pipeline operator tree is
for tools — optimize, inspect, ship — the inline shape is for speed.)
Fold/Foldable— the push side;Monoidderives folds.- Writer programs, producers, generators (
generate/Put: one unfold, three carriers — LazyList, Producer, Teller) are all streams; effect handlers forward the telling, so they are stream transformers. Take/pipe(Pipe.scala) — coroutine pipelines: tell meets await one element at a time, no channel, no materialization; the consumer drives, a finite consumer ends an infinite producer.Stage[I, O, A]is the transducer as a program;Stage.transduceis the skeleton they all share (state, a step that tells what the input is worth, a flush),Stage.mapAccumulatethe fs2-shaped 1:1 special case.Chunks[A] = Producer[Chunk[A]](Chunks.scala) — the tree steps per chunk, an element costs an array index: generators, transformers, zip, rechunk, fold, pipe; spec in specs/chunked-streams.md.
Channel— the queue between fibers;mergecombines streams by readiness (chunked merge: 10.7 us vs ZIO 45 on 2x500),bufferruns the producer ahead. Parking backpressure on JVM/Native; the Await-based JS channel keeps the same surface (Channel.scala per platform).Source[W]— an asynchronous stream as a program (Unit ! Writer % W + Async), the shape every streaming seam here speaks;a merge bjoins two of DIFFERENT element types into a source of their union, bounded by default (an endless source merged unbounded measured 1.27M elements produced for 10 consumed).- Everything runs on virtual threads by default; fork/join of 100 trivial tasks: 29 us (raw Loom 21, kyo 25, ZIO 50, cats-effect 140).
- Across machines:
Remoteships chunks over a socket into an ordinary local Channel, andCluster.distributespreads a chunked source over workers with per-chunk recompute on failure — the Aggregator merge is the cross-node contract (okay-cluster).
JMH, average time in us/op, lower is better. Versions: cats-effect 3.5.7, ZIO 2.1.14, kyo 0.16.2, atnos-eff 7.0.4, fs2 3.10.2. Full history, protocols and refuted experiments: src/jmh/history.tsv.
Bind chain — 10k left-nested flatMaps, built and run:
| Okay Eager | kyo | Okay Cont | Okay Free | cats Free | cats Eval | cats IO | ZIO | atnos |
|---|---|---|---|---|---|---|---|---|
| 5.1 | 58 | 89 | 95 | 129 | 136 | 153 | 181 | 260 |
(Okay Eager is the kyo trick as an OPT-IN encoding — import Eager.given — with the hazard stated: construction evaluates, so a self-referential program diverges before it runs, exactly what compare/TestLaziness catches kyo on (it runs 513 iterations at the CONSTRUCTION of an infinite program). Free/Eff keep the laziness contract; the user chooses per program.)
Reader — 10k asks:
| Okay | kyo Env | ZIO | cats Kleisli/Eval | atnos |
|---|---|---|---|---|
| 79 | 291 | 245 | 328 | 3123 |
Writer — 10k tells, collected:
| Okay | kyo Emit | cats WriterT/Chain | atnos |
|---|---|---|---|
| 163 | 215 | 1250 | 4054 |
(Okay and kyo in the right-nested shape a for-comprehension builds. The left-nested foldLeft shape is O(N²) in kyo — 362 099 / 364 313 — and Okay's rotation keeps it linear at 124 / 209; docs/benchmarks.md §2 has both rows and the mechanism.)
Choice — 2^13 branches, all collected (plain List is the floor):
| List | Okay | kyo | atnos |
|---|---|---|---|
| 580 | 1603 | 3834 | 5392 |
Fork/join — 100 trivial fibers (raw virtual threads are the floor):
| raw Loom | kyo | Okay | ZIO | cats IO |
|---|---|---|---|---|
| 21 | 25 | 29 | 50 | 140 |
Stream pipeline — map/filter/take(1000)/sum (Iterator is the floor):
| Iterator | Okay chunked | Okay elements | kyo Stream.range |
kyo singleton | ZIO | fs2 |
|---|---|---|---|---|---|---|
| 14 | 16.9 | 23.6 | 64† | 239 | 692 | 1410 |
(†kyo's own chunked source, measured in a later session against a 15.3 floor — 4.2x from the floor; docs/benchmarks.md §5.)
Merge — two 500-element streams merged by readiness:
| Okay chunked | ZIO | Okay elementwise | fs2 |
|---|---|---|---|
| 14.7 | 47 | 158 | 9031 |
Resource — 1000 bracketed acquire/use/release:
| Okay region | Okay bracket | ZIO | cats IO | kyo |
|---|---|---|---|---|
| 15.0 | 36 | 135 | 237 | 838 |
(kyo and Okay region in the right-nested shape; kyo's foldLeft lane is the same O(N²) trap as Reader/Writer, 9011.)
Generators — the 1000th Fibonacci number, element by element:
| Iterator | LazyList | Okay Producer | Okay LazyList | kyo | ZStream | fs2 |
|---|---|---|---|---|---|---|
| 12 | 13.5 | 18.4 | 35 | 61 | 172 | 245 |
Interop: cats, ZIO, kyo, fs2, Kafka, Spark, Flink, JDBC — and the JDK
itself (okay-java), where Aggregator IS java.util.stream.Collector
(supplier/accumulator/combiner/finisher against init/add/merge/present)
and Chunks crosses to Stream chunk-for-chunk, unboxed in both
directions for LongStream/IntStream/DoubleStream.
Everything below is built from the primitives above, and each is one module with its own page under docs/modules:
- text — total streaming lex (
okay-lex, BPE included), total lossless parse with O(damage) incremental reparse (okay-parse), oneSchemaserving JSON/CBOR/Markdown and JSON Schema (okay-codec). - models — completions as token streams over one transport seam,
two provider dialects, structured output that cuts generation
mid-stream (
okay-llm); retrieval with provenance by construction, the index an Aggregator (okay-rag); agents as programs — a tool call is an effect, context is a fold, policy lives in handlers (okay-agent). - MCP (
okay-mcp) — both ends of the Model Context Protocol: a server is anotherHandler[Tool], our tools are another server, resources are documents, prompts are conversation openings, sampling is theModeleffect; stdio and streamable HTTP (with server push over the GET stream), verified live against the protocol's reference server. - ui (
okay-ui) — the view as a value, the loop as a fold over merged sources, the renderer as a seam: one application on a terminal, under React, on a test host; forms derived from the sameSchemathat decodes them — which is what lets an MCP server ask the human (elicitation) and get a typed answer. - security (
okay-security) — authorization once: claims as values, JWT over a crypto seam, policies as an algebra, protection as a route wrapper the type system enforces, OAuth2 client flows — zero dependencies, the JDK carries the primitives. - wires — REST and WebSocket as programs (
okay-http), served by the JDK, Jetty or Netty behind one seam (okay-jetty,okay-netty); the distributed runtime (okay-cluster).
Building: sbt test runs everything — 845 tests across two dozen
modules, on the JVM, under Node and as a linked native binary (the
live suites — a local model, an npx-spawned MCP server — skip where
their endpoint is absent). Scala 3.7.4 (the floor is 3.6, for
the redesigned given syntax; the ceiling is okay-spark, which pins
Spark's Scala 2.13 artifacts) and sbt 1.13.0 (sbt 2 waits on
sbt-platform-deps, which supplies %%% and has no sbt 2 release).
.jvmopts gives the build 6g — the launcher's default 4g is shared by
zinc, the compiler and every module at once, and has run out mid-
compile. If you also build in IntelliJ, its Scala compile server has
its own separate 4g cap worth raising for the same reason.
Benchmarks: sbt 'Jmh/run .*FibBenchmark.*', comparisons in the
compare module (sbt 'compare/Jmh/run ...'); history and refuted
experiments in src/jmh/history.tsv.
Start here:
| User guide | the concepts, layer by layer — control, effects, streams, the upper modules |
| Tutorial | the same layers by use: worked, runnable examples |
| Typepedia | every core type and typeclass, with its meaning and the recurring gotchas |
| Capabilities | context functions as the wiring: doors, provide, wire — dependency injection with the container deleted |
| The theory of Okay | the textbook: the theories the library stands on, the scientists, the papers, and why each design decision |
Going deeper:
| Benchmarks | every measured case, why each number is what it is, and where the honest limits are |
| The cast that could not go | six encodings tried against one assertion; the five failures are the useful part |
| Specs | the living design documents, one per feature, refutations kept |
| history.tsv | the raw measurement log, refuted experiments included |
Per module — every satellite has its own page under docs/modules/: the interop bridges (cats, zio, kyo, fs2, java, spark, flink, kafka, jdbc), the text stack (lex, parse, codec), retrieval and agents (rag, llm, agent, mcp), the network (http, jetty, netty) and the distributed runtime (cluster). The docs index lists them all with one-line summaries.