# `toyffect`: A Definitive Implementation Guide `toyffect` is a minimalist effect system that separates program description from execution. It achieves this by representing computations as lazy data structures (`Toyffect` objects) which are then executed by a runtime (`Fiber`). This guide details *how* and *why* its core components work, including specifics and edge cases to consider for a correct implementation. #### 1. The Core Philosophy: Instructions as Data The central principle is to transform imperative code into declarative data. Programs are written as **generator functions** that `yield` plain JavaScript objects called **Instructions**. This makes the program logic a pure, testable description of a computation, deferring all execution to a separate runtime. **Why Generators?** They are chosen over alternatives for specific reasons: * **vs. `async/await`**: `async/await` is *eager* (it executes immediately). We need *lazy* descriptions to control *when* and *if* effects run. * **vs. Callbacks**: Generators allow us to write asynchronous logic in a linear, synchronous-looking style, avoiding "callback hell" and simplifying error handling. #### 2. The `Instruction` Set: The System's Language The `Instruction` type is a discriminated union that defines all possible operations. A robust implementation requires careful thought about each one. ```typescript type Instruction = | PromiseInstruction<any> | RequireInstruction<any> | GenInstruction<any> | ForkInstruction<any>; // WHY: The `f` function is a thunk `() => Promise` to ensure laziness. // SPECIFIC: It must accept an `AbortSignal` to allow for cancellation of long-running promises. interface PromiseInstruction<A> { readonly _tag: "promise"; readonly f: (signal: AbortSignal) => Promise<A>; } // WHY: Using a class constructor as a `tag` provides a unique, type-safe key for DI. // SPECIFIC: The runtime will use this class object to look up the implementation in a Map. interface RequireInstruction<T> { readonly _tag: "require"; readonly tag: new (...args: any[]) => T; } // WHY: This instruction enables sequential composition (`do`-notation style). // It lets one `Toyffect` run to completion before continuing the parent. interface GenInstruction<A> { readonly _tag: "gen"; readonly f: () => Generator<Instruction, A, any>; } // WHY: This is the primitive for concurrency. It starts a new logical thread of execution. interface ForkInstruction<A> { readonly _tag: "fork"; readonly toyffect: Toyffect<A, any>; } ``` #### 3. The `Toyffect` Class: The Program Blueprint This class is an immutable "recipe card" for a computation, holding the generator factory and its dependency context. ```typescript class Toyffect<A, R> { // SPECIFIC: Store a factory `() => Generator` because generators are single-use stateful iterators. // Each `.run()` must get a fresh generator. constructor( public readonly generatorFactory: () => Generator<Instruction, A, any>, public readonly context: Map<any, any> = new Map(), ) {} // WHY: Immutability is crucial. `provide` returns a *new* `Toyffect` instance. // This prevents spooky-action-at-a-distance and makes program configuration predictable. provide<S>(tag: new () => S, implementation: S): Toyffect<A, R> { const newContext = new Map([...this.context, [tag, implementation]]); return new Toyffect(this.generatorFactory, newContext); } // `.run()` is the "end of the world" function that kicks off execution. run(): Promise<Result<A>> { const fiber = new Fiber(this.generatorFactory(), this.context); return fiber.resolve(); } } type Result<A> = | { _tag: "succeeded"; value: A } | { _tag: "failed"; exception: unknown }; ``` #### 4. `Context` and DI: The `Symbol.iterator` Trick This is the most elegant part of the DI system. It makes dependency requests feel like a native language feature. **How it Works:** 1. A service `Tag` (e.g., `Logger`) is a class with a `static *[Symbol.iterator]()` method. 2. This makes the class constructor *itself* an iterable. 3. When a user writes `yield* Logger`, JavaScript's `yield*` delegation calls `Logger[Symbol.iterator]()`. 4. This special iterator yields a single `RequireInstruction` for itself. 5. The `Fiber` receives this instruction and looks up the `Logger` class in its context `Map`. ```typescript class Logger { static *[Symbol.iterator]() { yield { _tag: "require", tag: this }; } log(message: string): void { throw "Abstract method not implemented"; } } ``` #### 5. The `Fiber`: The Cooperative Multitasking Runtime The `Fiber` is the engine that executes the program. It's a cooperative multitasking scheduler that runs one effect at a time. **Key Implementation Details:** * **Trampoline Loop:** The core of `resolve` is a `while` loop, not recursion. This prevents stack overflows on deeply nested effects. * **Cooperative Yielding:** The generator `yield`s control back to the Fiber's loop after each instruction. The Fiber performs the effect and then resumes the generator. This is the "cooperative" part of the multitasking. * **Cancellation:** Each Fiber must manage an `AbortController`. This signal is passed to promise thunks and propagated to child fibers to enable graceful cancellation. ```typescript class Fiber<A> { private readonly iterator: Generator<Instruction, A, any>; private readonly context: Map<any, any>; private readonly controller = new AbortController(); public readonly signal = this.controller.signal; constructor(iterator: Generator<Instruction, A, any>, context: Map<any, any>) { this.iterator = iterator; this.context = context; } async resolve(): Promise<Result<A>> { let current = this.iterator.next(); while (!current.done) { const instruction = current.value; let result: any; try { switch (instruction._tag) { case "promise": result = await instruction.f(this.signal); break; case "require": result = this.context.get(instruction.tag); if (!result) throw new Error(`Service not provided: ${instruction.tag.name}`); break; // HOW IT WORKS: Sequential composition. Creates a new Fiber, but crucially `await`s its result. // This blocks the parent Fiber until the child completes. case "gen": const genFiber = new Fiber(instruction.f(), this.context); const genResult = await genFiber.resolve(); if (genResult._tag === "failed") throw genResult.exception; result = genResult.value; break; // HOW IT WORKS: Concurrency. Creates a new Fiber, starts it, but does *not* await. // The Fiber object itself is returned immediately as a "handle." case "fork": const forkFiber = new Fiber( instruction.toyffect.generatorFactory(), // SPECIFIC: Child context inherits from parent, but its own context takes precedence. new Map([...this.context, ...instruction.toyffect.context]), ); forkFiber.resolve(); // Start in the background, don't await. result = forkFiber; break; } current = this.iterator.next(result); } catch (e) { // SPECIFIC: On error, you must call .return() to ensure `finally` blocks // in the user's generator code are executed for cleanup. this.iterator.return?.(); this.controller.abort(); return { _tag: "failed", exception: e }; } } this.controller.abort(); return { _tag: "succeeded", value: current.value }; } } ``` #### 6. Building High-Level APIs User-friendly functions like `Toyffect.all` are composed from these low-level primitives. **`Toyffect.all` Implementation Logic:** 1. It is a `gen` effect that takes an array of other `Toyffect`s. 2. **Fork Phase:** It iterates through the array, `fork`ing each effect. This starts them all running in parallel and collects their Fiber handles. 3. **Join Phase:** After all effects are forked, it uses a final `promise` instruction that wraps a `Promise.all` around the `.resolve()` promises of all the collected Fiber handles. This waits for all concurrent tasks to complete, ensuring all results are gathered before proceeding. This `fork-then-join` pattern is fundamental for building safe, parallel operations in this model. --- # The Effect System You Could Have Invented: A Journey of Discovery When we first encounter an "effect system" like `Effect-TS` or even a smaller library like `toyffect`, it can feel like magic. The code is clean, dependencies appear out of thin air, and asynchrony seems effortless. But these powerful patterns aren't magic. They are the logical conclusion of solving a series of common programming problems, one step at a time. Today, we're going on that journey of discovery. We'll start with familiar, messy code and, by asking the right questions, we'll "invent" the core concepts of `toyffect` ourselves. By the end, you'll not only understand *how* it works, but *why* it has to work this way. ### Chapter 1: The Problem We All Face - Impurity Let's begin with a simple, everyday task: fetch some data and log it. ```typescript // The "normal" way async function fetchAndLogUser(userId: number) { try { // Side Effect 1: Networking const response = await fetch(`https://api.example.com/users/${userId}`) const user = await response.json() // Side Effect 2: Console I/O console.log("Fetched user:", user) return user } catch (error) { // Side Effect 3: Console I/O console.error("Failed to fetch user:", error) throw error } } ``` This code works, but it's a minefield of hidden problems: * **It's hard to test.** How do you verify `console.log` was called with the right data? You need to spy on the global console object. How do you test the error case without a network connection? You have to mock the global `fetch` API. It's fragile and cumbersome. * **It's not reusable.** What if tomorrow you need to log to a file or a logging service instead of the console? You have to find and change this function's code. * **It's not composable.** What if you want to run two `fetchAndLogUser` calls in parallel and retry if one fails? This simple function offers no tools to help you build more complex logic. The core issue is that our function does too much. It contains both our high-level business logic (the sequence of operations) and the low-level, messy details of execution (making network calls, writing to the console). Our first question is this: **How can we separate the *what* from the *how*?** How can we describe our program's logic without actually executing the side effects? ### Chapter 2: The First Leap - From Actions to Data Instead of our function *doing* things, what if it just returned a *description* of what it wants to do? Let's imagine a world where our function returns a simple data structure—a blueprint. ```typescript // A description of a fetch operation interface FetchInstruction { _tag: "fetch" url: string } // A description of a logging operation interface LogInstruction { _tag: "log" message: any } // A blueprint for our program is a sequence of instructions type Instruction = FetchInstruction | LogInstruction function describeFetchAndLog(userId: number): Instruction[] { // This function is now 100% pure! It has no side effects. // It just returns a value. return [ { _tag: "fetch", url: `https://api.example.com/users/${userId}` }, // Whoops! How do we tell the next step to use the result of the fetch? // We need the user data for the log instruction. ] } ``` We've made our function pure, which is a massive win for testability! We can now test `describeFetchAndLog` by just checking the array it returns. But this simple array approach has a critical flaw: it's not expressive enough. We can't describe how one step depends on the output of another. We need a way to describe a sequence of operations where each step might depend on the result of the one before it. We need a mechanism that can **pause** a computation, hand off a task to a runner, and then **resume** with the result. This "pausable function" is exactly what JavaScript **Generators** were designed for. ### Chapter 3: The Engine - Generators and the Interpreter Generators are functions that can be paused and resumed using the `yield` keyword. This is the perfect mechanism to build our system. Let's rewrite our program description as a generator. ```typescript // Our program, now a pausable recipe function* fetchAndLogGenerator(userId: number) { // Step 1: "yield" a description of the promise we want to run. const user = yield { _tag: "promise", f: () => fetch(...) } // --- PAUSE --- // The code stops here. An external "runner" will execute the promise. // When it's done, the runner will resume our generator, and the result // will be placed in the `user` variable. // Step 2: "yield" a description of the log operation. yield { _tag: "log", message: user } return user // The final result of our generator } ``` This is a huge breakthrough! Our logic is still pure and testable—it just `yield`s data. But now it's also dynamic. Notice we're yielding `{ f: () => fetch(...) }`, not `fetch(...)`. This is a crucial concept called **laziness**. The `fetch` call isn't made when the generator is written; it's only made when our interpreter decides to execute that instruction. This gives us complete control over when (and if) our effects run. Now, we need that interpreter. A "runner" that can take these yielded instructions and bring them to life. In `toyffect`, we call this the **`Fiber`**. The `Fiber`'s job is to be the chef that follows our recipe: 1. Start the generator. 2. Receive the first yielded `Instruction` object. 3. Look at the instruction's `_tag` to understand what to do. 4. Execute the real side effect (e.g., actually `await` the promise). 5. When the execution is complete, call `iterator.next(result)` to resume the generator, injecting the result back in. 6. Repeat this "trampoline" loop until the generator is done. We have just invented the core execution model of an effect system! Our program description is a `Toyffect`, and our runner is a `Fiber`. ### Chapter 4: The Dependency Puzzle - Inventing Type-Safe DI Our system is powerful, but our logic is still tied to implementation details like `fetch` and `console.log`. How can our program ask for a "service" without knowing *anything* about its implementation? Let's invent a new instruction: `Require`. ```typescript // Define unique "tags" for our services. // Strings are a bad idea (typos, no type safety). // Classes are perfect! They are both a value (a key) and a type. class Logger {} class ApiClient {} function* ourBusinessLogic() { // Ask for the services we need by their tag. const api = yield { _tag: "require", tag: ApiClient } const logger = yield { _tag: "require", tag: Logger } const user = yield* api.fetchUser(123) // Assume fetchUser is another effect logger.log(user) } ``` The `Fiber` can handle this. We'll give it a `Map` of services when it's created. When it sees a `Require` instruction, it just looks up the tag in its private context. How do we provide these services? We'll add a `.provide()` method to our `Toyffect` object. It won't run anything; it will just store the service implementation to be used later when `.run()` is finally called. ```typescript ourBusinessLogic() .provide(Logger, { log: (msg) => console.log(msg) }) .provide(ApiClient, { fetchUser: (id) => ... }) .run() // .run() creates the Fiber, passing it the provided services. ``` This is a fantastic separation of concerns. But we can make the syntax even cleaner. The line `yield { _tag: "require", tag: Logger }` is a bit clunky. This leads to the final, most elegant "aha!" moment. What if we could just write `yield* Logger`? The `yield*` keyword delegates to another generator or iterable. So, what if we make the `Logger` class *itself* an iterable that yields our `Require` instruction? ```typescript // This is the genius behind toyffect's Context.Tag class Logger { // A static iterator makes the CLASS itself iterable. static *[Symbol.iterator]() { // It yields exactly one thing: the instruction to require itself. yield { _tag: "require", tag: this } // `this` is the Logger class } } // Now our program is stunningly beautiful: function* ourBusinessLogic() { const logger = yield* Logger logger.log("This is so declarative!") } ``` By leveraging a standard JavaScript feature, we've created a tiny, type-safe **Domain-Specific Language (DSL)** for requesting dependencies. It's elegant because it's not a library-specific trick; it's just clever use of the language itself. ### Chapter 5: Taming Concurrency - The Birth of Child Fibers Our final challenge: how to run things in parallel? Imagine we want to run `effectA` and `effectB` at the same time. If our `Fiber` simply processes instructions one by one, `effectA` will have to finish completely before `effectB` can even start. That's sequential, not parallel. To achieve parallelism, our main `Fiber` must be able to spawn other `Fiber`s. When it receives an instruction to run something concurrently (let's call it a `Fork` instruction), it must *not* wait. Instead, it should: 1. Create a **new, independent child `Fiber`** for the concurrent task. 2. Start this child `Fiber` running in the background (by calling its `resolve` method). 3. Immediately return the `childFiber` object itself back to the parent program, without waiting for it to finish. The `Toyffect.all` helper can then be implemented like this: ```typescript // The logic of Toyffect.all, reinvented: function* all(effects) { const childFibers = [] for (const effect of effects) { // 1. Fork it! This starts the effect but returns immediately. const childFiber = yield { _tag: "fork", toyffect: effect } childFibers.push(childFiber) } // 2. Now that all children are running, wait for them all to finish. const results = yield { _tag: "promise", f: () => Promise.all(childFibers.map(f => f.resolve())) } return results } ``` This is **structured concurrency**. The parent `Fiber` "owns" its children. If the parent is cancelled (for example, by an `AbortController`), it can be designed to automatically cancel all the child fibers it created. This prevents orphaned promises and memory leaks, making concurrent code safe and predictable. ### Your Journey's End We've done it. We started with a messy, impure function and, by solving a series of logical problems, we have arrived at the core architecture of a modern effect system: 1. **Effects as Data:** We separated logic from execution by describing our programs as lazy, inert data structures. 2. **The Interpreter:** We used a `Fiber` as a runtime to bring those descriptions to life in a controlled, pausable way. 3. **Type-Safe DI:** We built a beautiful dependency injection system by combining classes-as-tags with the `[Symbol.iterator]` language feature. 4. **Structured Concurrency:** We enabled safe parallelism by allowing fibers to spawn and manage child fibers. These patterns aren't magic. They are the product of principled engineering, designed to solve real-world problems. And now that you've walked the path of their discovery, you don't just know *what* they are; you know *why* they exist. You could have invented them, too.