A TypeScript transformer for use with roblox-ts that implements a do-notation by transforming generator functions with yield* expressions into continuation-passing style.
In languages that lack native support for capturing and storing continuations, multi-shot algebraic effects may be simulated by writing programs in continuation-passing style. The ability to write imperative-style code can then be recovered by building chains of continuations using native language constructs. In the case of JavaScript, generator functions provide a straightforward way to implement this hiding:
// Example using Effect-ts
const program = pipe(
Effect.succeed(2),
Effect.map(n => n * 3),
Effect.flatMap(n => Effect.succeed(n + 1)),
Effect.map(n => `Result: ${n}`)
)
const program = Effect.gen(function* () {
const n = yield* Effect.succeed(2)
const doubled = n * 3
const incremented = yield* Effect.succeed(doubled + 1)
return `Result: ${incremented}`
})The purpose of this transformer is to provide more control over how sugar for effect primitives is interpreted. By sitting between source code written using multi-shot algebraic effects and the TypeScript AST upon on by roblox-ts, it aims to make ahead-of-time structural changes that would otherwise be burdensome.
Once installed, rbxts-do-transformer, along with any configuration options, must be added to your tsconfig.json.
"compiler options": {
...
"plugins": [
{
"transform": "rbxts-do-transformer",
// "debug": bool,
// "verbose": bool,
// "adoAlgorithm": "heuristic" | "optimal"
}
],
}rbxts-do-transformer is easily accessed through the TypeScript compiler API:
import ts from "typescript";
import { doTransformer } from "@rbxts/do-transformer";
const result = ts.transpileModule(sourceCode, {
compilerOptions: { /* ... */ },
transformers: {
before: [doTransformer]
}
});The transformer recognizes calls to do_() or ado()_ with a generator function:
do_(function* () {
// Program
})
ado_(function* () {
// Program
})Basic assignment
const program = do_(function* () {
const { x, y } = yield* getCoordinates();
const [ a, b ] = yield* getTuple();
return x + a;
});
// Transformed
const program = seq(getCoordinates(), (_t0) => {
return (() => {
const { x, y } = _t0;
return seq(getTuple(), (_t1) => {
return (() => {
const [a, b] = _t1;
return ret(x + a);
})();
});
})();
});Transforming using the apply higher-order effect
const program = ado_(function* () {
const x = yield* getX();
const y = yield* getY();
const z = yield* getZ(x, y);
const w = yield* getW();
return z + w;
});
// Transformed
const program = op("apply", (z, w) => z + w, [
seq(
op("apply", (x, y) => getZ(x, y), [getX(), getY()]),
(_v) => _v,
),
getW(),
]);Mixed control flow
const program = do_(function* () {
const a = yield* get();
for (let i = 0; i < a.upper; i++) {
do {
yield* log(`${i}`);
} while (yield* fetch("resource"))
}
switch (a.type) {
case "ok":
yield* set("No errors");
case "error":
yield* set("Errors");
break;
case "fuzzy":
yield* set("Fuzzy");
default:
yield* set("Unknown");
}
return "Done";
});
// Transformed
const program = seq(get(), (a) => {
return (() => {
let i = 0;
const _k0 = () =>
(() => {
const _k2 = () => ret("Done");
const _t11 = a.type;
const _t3 = () =>
seq(set("No errors"), () => {
return _t4();
});
const _t4 = () =>
seq(set("Errors"), () => {
return _k2();
});
const _t5 = () =>
seq(set("Fuzzy"), () => {
return _t6();
});
const _t6 = () =>
seq(set("Unknown"), () => {
return _k2();
});
return _t11 === "ok" ? _t3() : _t11 === "error" ? _t4() : _t11 === "fuzzy" ? _t5() : _t6();
})();
const _loop0 = () =>
i < a.upper
? (() => {
const _k1 = () =>
(() => {
i++;
return _loop0();
})();
const _loop1 = () =>
seq(log(`${i}`), () => {
return seq(fetch("resource"), (_t2) => {
return _t2 ? _loop1() : _k1();
});
});
return _loop1();
})()
: _k0();
return _loop0();
})();
});- Transforms
yield*expressions into CPS combinators - Supports complex control flow (if/else, loops, break/continue)
- Automatic insertion of applicative operations
- Debug tracing
The transformer performs a small number of transformations:
- Yield Extraction: Recursively extracts
yield*expressions from nested positions within statements - Statement Sequencing: Converts imperative statements into
seqchains - Control Flow: Transforms if/else and loops into functional equivalents
- Variable Binding: Lifts out variable declarations and threads them through continuations
- Does not support aliases or namespaces for
do_andado_. - Requires
seq,ret, andapplyto be in scope at runtime - Does not support
yieldwithout asterisk (though this may become a warning rather than an error in the future)
MIT