Skip to content

Instantly share code, notes, and snippets.

@baetheus
Last active August 24, 2022 03:24
Show Gist options
  • Select an option

  • Save baetheus/07a7d1e41626fe92900b450fe95b2dcd to your computer and use it in GitHub Desktop.

Select an option

Save baetheus/07a7d1e41626fe92900b450fe95b2dcd to your computer and use it in GitHub Desktop.
briancavalier fx but in deno
// 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