# npm
npm i @susisu/effectful
# yarn
yarn add @susisu/effectful
# pnpm
pnpm add @susisu/effectful
You can register effects by extending EffectRegistry<T>
.
declare module "@susisu/effectful" {
interface EffectRegistry<T> {
// Reads a file and returns its content as a string.
read: {
filename: string;
constraint: (x: string) => T; // constrains `T = string`
};
// Prints a message and returns void.
print: {
message: string;
constraint: (x: void) => T; // constrains `T = void`
};
}
}
This is for later convenience.
Here "smart constructors" are functions that construct atomic computations.
Eff<T, Row>
is the type of compuations that perform effects in Row
(a union type of effect keys) and return T
.
import type { Eff } from "@susisu/effectful";
import { perform } from "@susisu/effectful";
function read(filename: string): Eff<string, "read"> {
return perform({
key: "read",
data: {
filename,
constraint: (x) => x, // `constraint` should be an identity function
},
});
}
function print(message: string): Eff<void, "print"> {
return perform({
key: "print",
data: {
message,
constraint: (x) => x,
},
});
}
You can write effectful computations using generators, like async / await for Promises.
function* getSize(filename: string): Eff<number, "read"> {
// Use `yield*` to perform effects.
const contents = yield* read(filename);
return contents.length;
}
function* main(): Eff<void, "read" | "print"> {
// `yield*` can also be used to compose computations.
const size = yield* getSize("./examples/input.txt");
yield* print(`The file contains ${size} characters.`);
}
Write interpreters (or effect handlers) so that effects can take actual effect.
import type { EffectKey } from "@susisu/effectful";
import { interpret, waitFor, bind } from "@susisu/effectful";
import { readFile } from "fs/promises";
// Translates `read` effect to `async` effect.
function interpretRead<Row extends EffectKey, T>(
comp: Eff<T, Row | "read">,
): Eff<T, Row | "async"> {
return interpret<"read", Row | "async", T>(comp, {
read(effect, resume, abort) {
// `bind` is a function that works like `Promise.prototype.then`.
return bind(
waitFor(readFile(effect.data.filename, "utf-8")),
// Use `constraint: (x: string) => S` to pass `contents: string` to `resume: (value: S) => ...`.
(contents) => resume(effect.data.constraint(contents)),
abort,
);
},
});
}
// Interprets `print` effect as output to the console.
function interpretPrint<Row extends EffectKey, T>(comp: Eff<T, Row | "print">): Eff<T, Row> {
return interpret<"print", Row, T>(comp, {
print(effect, resume) {
console.log(effect.data.message);
return resume(effect.data.constraint(undefined));
},
});
}
Run our main
computation by interpreting the effects.
import { runAsync } from "@susisu/effectful";
runAsync(interpretPrint(interpretRead(main())));