Last active
          August 17, 2025 12:08 
        
      - 
      
 - 
        
Save tokland/17c0ca65509bb4bb06992f1ce94c7e86 to your computer and use it in GitHub Desktop.  
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | import { promises as fsP } from "node:fs"; | |
| import { promises as readlineP } from "node:readline"; | |
| /** | |
| * @module EffectSystem | |
| * | |
| * @overview | |
| * This module defines a lightweight, composable effect system for managing asynchronous side effects | |
| * in a structured and declarative way. It enables the creation, composition, and execution of effectful | |
| * operations—such as I/O, user interaction, and file system access—while maintaining strong type safety | |
| * and separation of concerns. | |
| * | |
| * @goals | |
| * - Define a generic infrastructure for modeling asynchronous effects using TypeScript. | |
| * - Allow effects to be composed using functional combinators like `map` and `flatMap`. | |
| * - Provide a mechanism to execute composed effects with concrete implementations. | |
| * - Support cancellation infrastructure for future extensibility (e.g., with cancellable promises). | |
| * | |
| * @core concepts | |
| * - `BaseIEffects`: A generic type that defines the shape of an effect interface, mapping keys to async functions. | |
| * - `Effect`: A class representing a single or composed effect, parameterized by the effect interface and result type. | |
| * - `create`: Method to instantiate a concrete effect from an effect key and arguments. | |
| * - `map` / `flatMap`: Functional combinators for transforming and chaining effects. | |
| * - `run`: Executes the effect using a provided implementation and success callback. | |
| * | |
| * @example | |
| * ```ts | |
| * interface MyEffects { | |
| * print(message: string): Promise<void>; | |
| * readFile(options: { filename: string }): Promise<string>; | |
| * } | |
| * | |
| * const effect = Effect.build<MyEffects>(); | |
| * | |
| * const compositeEffect = effect | |
| * .flatMap(() => effect.create("print", "Hello!")) | |
| * .flatMap(() => effect.create("readFile", { filename: "input.txt" })) | |
| * .map((contents) => contents.toUpperCase()); | |
| * | |
| * const realImpl: MyEffects = { | |
| * print: async (message) => console.log(message), | |
| * readFile: async ({ filename }) => "file contents", | |
| * }; | |
| * | |
| * compositeEffect.run(realImpl, (result) => { | |
| * console.log("Final result:", result); | |
| * }); | |
| * ``` | |
| * | |
| * @todo | |
| * - Implement cancellation support for effects. | |
| * - Add error branch to run. | |
| * - Extend the effect system with more combinators for sequential, parallel execution, retries, etc. | |
| * | |
| */ | |
| /* Infrastructure */ | |
| function debug(...args: any[]) { | |
| console.debug("[DEBUG]", ...args); | |
| } | |
| type BaseIEffects<Keys extends keyof any> = { | |
| [K in Keys]: (...args: any[]) => Promise<any>; | |
| }; | |
| type CancelFn = () => void; | |
| // We using standard promises, so we cannot cancel them, but let's add to the infrastructure anyway | |
| // A library to have cancellable promises: 'real-cancellable-promise'. | |
| const cancelFn: CancelFn = () => {}; | |
| class Effect<IEffects extends BaseIEffects<keyof IEffects>, AllEfectKind extends keyof IEffects, ResultType> { | |
| constructor( | |
| private options: { | |
| run: (implementations: IEffects, onSuccess: (value: ResultType) => void) => CancelFn; | |
| } | |
| ) {} | |
| static build<SomeEffects extends BaseIEffects<keyof SomeEffects>>(): Effect<SomeEffects, never, void> { | |
| return new Effect<SomeEffects, never, void>({ | |
| run: (_implementations, onSuccess) => { | |
| onSuccess(undefined); | |
| return cancelFn; | |
| }, | |
| }); | |
| } | |
| create<Kind extends keyof IEffects>( | |
| kind: Kind, | |
| ...args: Parameters<IEffects[Kind]> | |
| ): Effect<IEffects, Kind, Awaited<ReturnType<IEffects[Kind]>>> { | |
| return new Effect({ | |
| run: (implementations, onSuccess) => { | |
| const effectFn = implementations[kind]; | |
| debug(`Running effect '${String(kind)}':`, ...args); | |
| const value = effectFn(...args); | |
| value.then((result) => { | |
| debug(`Effect '${String(kind)}' completed with result:`, result); | |
| onSuccess(result); | |
| }); | |
| return cancelFn; | |
| }, | |
| }); | |
| } | |
| run(implementations: IEffects, onSuccess: (value: ResultType) => void): CancelFn { | |
| return this.options.run(implementations, onSuccess); | |
| } | |
| /* Combination methods */ | |
| map<ResultType2>( | |
| getValue: (value: ResultType) => ResultType2 | |
| ): Effect<IEffects, AllEfectKind, ResultType2> { | |
| return new Effect({ | |
| run: (implementations, onSuccess) => { | |
| return this.options.run(implementations, (result) => { | |
| const value = getValue(result); | |
| onSuccess(value); | |
| }); | |
| }, | |
| }); | |
| } | |
| flatMap<EffectResultKind extends keyof IEffects, ReturnType2>( | |
| getEffect: (value: ResultType) => Effect<IEffects, EffectResultKind, ReturnType2> | |
| ): Effect<IEffects, AllEfectKind | EffectResultKind, ReturnType2> { | |
| return new Effect({ | |
| run: (implementations, onSuccess) => { | |
| return this.options.run(implementations, (result) => { | |
| const effect = getEffect(result); | |
| effect.run(implementations, onSuccess); | |
| }); | |
| }, | |
| }); | |
| } | |
| } | |
| /* Example usage */ | |
| interface MyEffects { | |
| print(message: string): Promise<void>; | |
| prompt(promptMessage: string): Promise<string>; | |
| readFile(options: { filename: string }): Promise<string>; | |
| writeFile(options: { filename: string; contents: string }): Promise<number>; | |
| } | |
| const effect = Effect.build<MyEffects>(); | |
| // Inferred as Effect<MyEffects, "print" | "prompt" | "readFile" | "writeFile", { written: number }> | |
| function readFileAndPrint() { | |
| return effect | |
| .flatMap(() => effect.create("print", "Hi, this is a composite effect!")) | |
| .flatMap(() => effect.create("prompt", "Please enter the filename: ")) | |
| .flatMap((filename) => effect.create("readFile", { filename })) | |
| .flatMap((contents) => effect.create("writeFile", { filename: "output.txt", contents: contents })) | |
| .map((bytesWritten) => ({ written: bytesWritten })); | |
| } | |
| // It's the time to execute the composite effect! We need to pass a concrete implementation of | |
| // the effects to the 'run' method along with a callback to handle the success result. | |
| class MyEffectsRealImpl implements MyEffects { | |
| async prompt(promptMessage: string) { | |
| const filename = process.env["FILENAME"]; | |
| if (filename) return filename; | |
| const readline = readlineP.createInterface({ input: process.stdin, output: process.stdout }); | |
| try { | |
| return await readline.question(promptMessage); | |
| } finally { | |
| readline.close(); | |
| } | |
| } | |
| async print(message: string) { | |
| console.log(message); | |
| } | |
| async readFile(options: { filename: string }) { | |
| return fsP.readFile(options.filename, "utf8").then((s) => s.trim()); | |
| } | |
| async writeFile(options: { filename: string; contents: string }) { | |
| await fsP.writeFile(options.filename, options.contents, "utf8"); | |
| return options.contents.length; | |
| } | |
| } | |
| function runExample() { | |
| const compositeEffect = readFileAndPrint(); | |
| const implementation = new MyEffectsRealImpl(); | |
| compositeEffect.run(implementation, (result) => { | |
| console.log("Composite effect completed successfully with value:", result); | |
| }); | |
| } | |
| if (require.main === module) { | |
| runExample(); | |
| } | 
  
    Sign up for free
    to join this conversation on GitHub.
    Already have an account?
    Sign in to comment