Last active
August 24, 2022 03:24
-
-
Save baetheus/07a7d1e41626fe92900b450fe95b2dcd to your computer and use it in GitHub Desktop.
briancavalier fx but in deno
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
| // See https://github.com/briancavalier/fx | |
| export type Effect<T, A, R> = { | |
| type: T; | |
| arg: A; | |
| [Symbol.iterator](): Generator<Effect<T, A, R>, R, R>; | |
| }; | |
| export function effect<T, A, R>(type: T, arg: A): Effect<T, A, R> { | |
| return { | |
| type, | |
| arg, | |
| *[Symbol.iterator]() { | |
| return yield this; | |
| }, | |
| }; | |
| } | |
| export interface Fx<Y, R, A> { | |
| [Symbol.iterator](): Iterator<Y, R, A>; | |
| } | |
| export const fx = <Y, R, A>(f: () => Generator<Y, R, A>): Fx<Y, R, A> => ({ | |
| [Symbol.iterator]: f, | |
| }); | |
| export const of = <R>(r: R): Fx<never, R, never> => ({ | |
| // deno-lint-ignore require-yield | |
| *[Symbol.iterator]() { | |
| return r; | |
| }, | |
| }); | |
| export const run = <R, A>(fx: Fx<never, R, A>): R => | |
| fx[Symbol.iterator]().next().value; | |
| export const handle = <Y, R, A, B, Y2>( | |
| f: Fx<Y, R, A>, | |
| handler: (effect: Y) => Fx<Y2, A, unknown>, | |
| ): Fx<Y2, R, B> => | |
| fx(function* () { | |
| const i = f[Symbol.iterator](); | |
| let ir = i.next(); | |
| while (!ir.done) ir = i.next(yield* handler(ir.value)); | |
| return ir.value; | |
| }); | |
| export type Task<A> = (k: (a: A) => void) => Dispose; | |
| export type Dispose = () => void; | |
| export type Async<A> = Effect<"Async", Task<A>, A>; | |
| export const async = <A>(t: Task<A>): Async<A> => effect("Async", t); | |
| export type Fiber<A> = [Dispose, Promise<A>]; | |
| export const dispose = <A>([dispose]: Fiber<A>): void => dispose(); | |
| export const promise = <A>([, promise]: Fiber<A>): Promise<A> => promise; | |
| // eslint-disable-next-line @typescript-eslint/no-empty-function | |
| const noDispose: Dispose = () => {}; | |
| export const fork = <R, A, Y extends Async<unknown>>( | |
| f: Fx<Y, R, A>, | |
| ): Fx<never, Fiber<R>, never> => | |
| // deno-lint-ignore require-yield | |
| fx(function* () { | |
| let _dispose: Dispose = noDispose; | |
| const stepAsync = async ( | |
| i: Iterator<Y, R, A>, | |
| ir: IteratorResult<Y, R>, | |
| ): Promise<R> => { | |
| if (ir.done) return ir.value; | |
| const a = await new Promise((resolve) => { | |
| _dispose = ir.value.arg(resolve); | |
| }); | |
| return stepAsync(i, i.next(a as A)); | |
| }; | |
| const i = f[Symbol.iterator](); | |
| const dispose = () => _dispose(); | |
| const promise = stepAsync(i, i.next()).then((x) => { | |
| _dispose = noDispose; | |
| return x; | |
| }); | |
| return [dispose, promise]; | |
| }); | |
| // EXAMPLE | |
| //--------------------------------------------------------------- | |
| // effects | |
| // Note that Read and Print have no implementation | |
| // They only represent a signature. For example, Read represents | |
| // the signature void -> string, i.e. it produces a string out of | |
| // nowhere. Print represents the signature string -> void, i.e. | |
| // it consumes a string. | |
| type Read = Effect<"Read", void, string>; | |
| const read: Read = effect("Read", undefined); | |
| type Print = Effect<"Print", string, void>; | |
| const print = (s: string): Print => effect("Print", s); | |
| type Exit = Effect<"Exit", number, void>; | |
| const exit = (n: number): Exit => effect("Exit", n); | |
| //--------------------------------------------------------------- | |
| // main program | |
| // It does what it says: loops forever, `Read`ing strings and then | |
| // `Print`ing them. Remember, though, that Read and Print have | |
| // no meaning. | |
| // The program cannot be run until all its effects have been | |
| // assigned a meaning, which is done by applying effect handlers | |
| // to main for its effects, Read and Print. | |
| const main = fx(function* () { | |
| let count = 0; | |
| while (true) { | |
| yield* print(`${count++} > `); | |
| const s = yield* read; | |
| if (s.match(/exit/i)) { | |
| yield* print(`Exiting in 1 second`); | |
| yield* exit(1000); | |
| } | |
| if (s.match(/hello/i)) { | |
| yield* effect("Hello" as const, undefined); | |
| } | |
| yield* print(`${s}`); | |
| } | |
| }); | |
| //--------------------------------------------------------------- | |
| // effect handler | |
| // This handler handles the Print and Read effects. It handles | |
| // Print by simply printing the string to process.stdout. | |
| // However, it handles Read by introducing *another effect*, | |
| // in this case, Async, to read a line asynchronously from | |
| // process.stdin. | |
| // So, handled still isn't runnable. We need to handle the | |
| // Async effect that this handler introduced. | |
| const decoder = new TextDecoder(); | |
| const encoder = new TextEncoder(); | |
| const handled = handle(main, function* (effect) { | |
| switch (effect.type) { | |
| case "Print": | |
| return Deno.stdout.writeSync(encoder.encode(effect.arg)); | |
| case "Read": { | |
| const buf = new Uint8Array(1024); | |
| Deno.stdin.readSync(buf); | |
| return decoder.decode(buf); | |
| } | |
| case "Exit": { | |
| return yield* async(() => { | |
| const handle = setTimeout(() => Deno.exit(0), effect.arg); | |
| return () => clearTimeout(handle); | |
| }); | |
| } | |
| case "Hello": { | |
| return console.error(`Unknown effect of type ${effect.type}`); | |
| } | |
| } | |
| }); | |
| // Finally, we can handle the Async effect using the | |
| // provided fork effect handler. | |
| const runnable = fork(handled); | |
| // Since runnable produces no effects, we can run it. | |
| run(runnable); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment