Skip to content

Commit 61ef163

Browse files
committed
✨ feat: Add Effected#pipe
1 parent 60abecc commit 61ef163

File tree

7 files changed

+701
-275
lines changed

7 files changed

+701
-275
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,35 @@ const welcomeMessage = getUserName.zip(
13941394

13951395
Just like `.andThen()`, `.zip()` also allows you to use a generator function or another effected program as the handler, which will be flattened automatically.
13961396

1397+
When built-in methods aren’t sufficient for your needs, you can create custom transformers and chain them with existing effected programs using the `.pipe(...fs)` method. This allows you to apply multiple transformations in a clean, functional style (inspired by [Effect](https://effect.website/docs/getting-started/building-pipelines/#the-pipe-method)):
1398+
1399+
```typescript
1400+
const delay =
1401+
(ms: number) =>
1402+
<E extends Effect, R>(
1403+
effected: Effected<E, R>,
1404+
): Effected<E | Default<Effect<"delay", [ms: number], void>>, R> =>
1405+
effect<[ms: number], void>()("delay", {
1406+
defaultHandler: ({ resume }, ms) => {
1407+
setTimeout(resume, ms);
1408+
},
1409+
})(ms).andThen(() => effected);
1410+
1411+
const withLog =
1412+
(message: string) =>
1413+
<E extends Effect, R>(effected: Effected<E, R>): Effected<E, R> =>
1414+
effected.tap((value) => {
1415+
console.log(`${message}: ${String(value)}`);
1416+
});
1417+
1418+
// Add a delay of 1000ms to the effected program
1419+
console.log(await Effected.of(42).pipe(delay(1000)).runAsync());
1420+
1421+
// You can use multiple transformers in `.pipe()`
1422+
await Effected.of(42).pipe(delay(1000), withLog("Result")).runAsync();
1423+
// Result: 42
1424+
```
1425+
13971426
### Pipeline Syntax V.S. Generator Syntax
13981427

13991428
Both pipeline syntax and generator syntax are valid approaches for working with effected programs in tinyeffect. Each approach has distinct advantages:

package-lock.json

Lines changed: 383 additions & 264 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,17 @@
4747
},
4848
"devDependencies": {
4949
"@commitlint/cli": "^19.8.0",
50-
"@typescript-eslint/parser": "^8.27.0",
50+
"@typescript-eslint/parser": "^8.28.0",
5151
"@vitest/coverage-v8": "^3.0.9",
5252
"@vitest/ui": "^3.0.9",
5353
"cpy-cli": "^5.0.0",
54-
"effect": "^3.14.1",
55-
"eslint": "^9.22.0",
54+
"effect": "^3.14.2",
55+
"eslint": "^9.23.0",
5656
"eslint-config-prettier": "^10.1.1",
57-
"eslint-import-resolver-typescript": "^4.2.2",
58-
"eslint-plugin-import-x": "^4.9.1",
59-
"eslint-plugin-jsdoc": "^50.6.8",
60-
"eslint-plugin-prettier": "^5.2.3",
57+
"eslint-import-resolver-typescript": "^4.2.4",
58+
"eslint-plugin-import-x": "^4.9.3",
59+
"eslint-plugin-jsdoc": "^50.6.9",
60+
"eslint-plugin-prettier": "^5.2.5",
6161
"eslint-plugin-sonarjs": "^3.0.2",
6262
"eslint-plugin-sort-destructure-keys": "^2.0.0",
6363
"globals": "^16.0.0",
@@ -69,7 +69,7 @@
6969
"ts-blank-space": "^0.6.1",
7070
"tsc-alias": "^1.8.11",
7171
"typescript": "^5.8.2",
72-
"typescript-eslint": "^8.27.0",
72+
"typescript-eslint": "^8.28.0",
7373
"typroof": "^0.5.1",
7474
"vitest": "^3.0.9"
7575
}

src/effected.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,77 @@ export class Effected<out E extends Effect, out R> implements Iterable<E, R, unk
11621162
with(handler: (effected: any) => unknown) {
11631163
return handler(this);
11641164
}
1165+
1166+
/**
1167+
* Pipe the effected program through a series of functions.
1168+
* @returns
1169+
*/
1170+
pipe<A, B = never>(this: A, ab: (a: A) => B): B;
1171+
pipe<A, B = never, C = never>(this: A, ab: (a: A) => B, bc: (b: B) => C): C;
1172+
// prettier-ignore
1173+
pipe<A, B = never, C = never, D = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D;
1174+
// prettier-ignore
1175+
pipe<A, B = never, C = never, D = never, E = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E): E;
1176+
// prettier-ignore
1177+
pipe<A, B = never, C = never, D = never, E = never, F = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F): F;
1178+
// prettier-ignore
1179+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G): G;
1180+
// prettier-ignore
1181+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never, H = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H): H;
1182+
// prettier-ignore
1183+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never, H = never, I = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I): I;
1184+
// prettier-ignore
1185+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never, H = never, I = never, J = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I, ij: (i: I) => J): J;
1186+
// prettier-ignore
1187+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never, H = never, I = never, J = never, K = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I, ij: (i: I) => J, jk: (j: J) => K): K;
1188+
// prettier-ignore
1189+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never, H = never, I = never, J = never, K = never, L = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I, ij: (i: I) => J, jk: (j: J) => K, kl: (k: K) => L): L;
1190+
// prettier-ignore
1191+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never, H = never, I = never, J = never, K = never, L = never, M = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I, ij: (i: I) => J, jk: (j: J) => K, kl: (k: K) => L, lm: (l: L) => M): M;
1192+
// prettier-ignore
1193+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never, H = never, I = never, J = never, K = never, L = never, M = never, N = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I, ij: (i: I) => J, jk: (j: J) => K, kl: (k: K) => L, lm: (l: L) => M, mn: (m: M) => N): N;
1194+
// prettier-ignore
1195+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never, H = never, I = never, J = never, K = never, L = never, M = never, N = never, O = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I, ij: (i: I) => J, jk: (j: J) => K, kl: (k: K) => L, lm: (l: L) => M, mn: (m: M) => N, no: (n: N) => O): O;
1196+
// prettier-ignore
1197+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never, H = never, I = never, J = never, K = never, L = never, M = never, N = never, O = never, P = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I, ij: (i: I) => J, jk: (j: J) => K, kl: (k: K) => L, lm: (l: L) => M, mn: (m: M) => N, no: (n: N) => O, op: (o: O) => P): P;
1198+
// prettier-ignore
1199+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never, H = never, I = never, J = never, K = never, L = never, M = never, N = never, O = never, P = never, Q = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I, ij: (i: I) => J, jk: (j: J) => K, kl: (k: K) => L, lm: (l: L) => M, mn: (m: M) => N, no: (n: N) => O, op: (o: O) => P, pq: (p: P) => Q): Q;
1200+
// prettier-ignore
1201+
pipe<A, B = never, C = never, D = never, E = never, F = never, G = never, H = never, I = never, J = never, K = never, L = never, M = never, N = never, O = never, P = never, Q = never, R = never>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, ef: (e: E) => F, fg: (f: F) => G, gh: (g: G) => H, hi: (h: H) => I, ij: (i: I) => J, jk: (j: J) => K, kl: (k: K) => L, lm: (l: L) => M, mn: (m: M) => N, no: (n: N) => O, op: (o: O) => P, pq: (p: P) => Q, qr: (q: Q) => R): R;
1202+
pipe(...args: ((value: any) => any)[]): any {
1203+
// Optimization inspired by Effect
1204+
// https://github.com/Effect-TS/effect/blob/f293e97ab2a26f45586de106b85119c5d98ab4c7/packages/effect/src/Pipeable.ts#L491-L524
1205+
switch (args.length) {
1206+
case 0:
1207+
return this;
1208+
case 1:
1209+
return args[0]!(this);
1210+
case 2:
1211+
return args[1]!(args[0]!(this));
1212+
case 3:
1213+
return args[2]!(args[1]!(args[0]!(this)));
1214+
case 4:
1215+
return args[3]!(args[2]!(args[1]!(args[0]!(this))));
1216+
case 5:
1217+
return args[4]!(args[3]!(args[2]!(args[1]!(args[0]!(this)))));
1218+
case 6:
1219+
return args[5]!(args[4]!(args[3]!(args[2]!(args[1]!(args[0]!(this))))));
1220+
case 7:
1221+
return args[6]!(args[5]!(args[4]!(args[3]!(args[2]!(args[1]!(args[0]!(this)))))));
1222+
case 8:
1223+
return args[7]!(args[6]!(args[5]!(args[4]!(args[3]!(args[2]!(args[1]!(args[0]!(this))))))));
1224+
case 9:
1225+
return args[8]!(
1226+
args[7]!(args[6]!(args[5]!(args[4]!(args[3]!(args[2]!(args[1]!(args[0]!(this)))))))),
1227+
);
1228+
default: {
1229+
// eslint-disable-next-line @typescript-eslint/no-this-alias
1230+
let result = this;
1231+
for (let i = 0, len = args.length; i < len; i++) result = args[i]!(result);
1232+
return result;
1233+
}
1234+
}
1235+
}
11651236
}
11661237

11671238
interface EffectedDraft<

test/README.example.proof.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,34 @@ test("Effects without generators", () => {
927927
>,
928928
);
929929
}
930+
931+
{
932+
const delay =
933+
(ms: number) =>
934+
<E extends Effect, R>(
935+
effected: Effected<E, R>,
936+
): Effected<E | Default<Effect<"delay", [ms: number], void>>, R> =>
937+
effect<[ms: number], void>()("delay", {
938+
defaultHandler: ({ resume }, ms) => {
939+
setTimeout(resume, ms);
940+
},
941+
})(ms).andThen(() => effected);
942+
943+
const withLog =
944+
(message: string) =>
945+
<E extends Effect, R>(effected: Effected<E, R>): Effected<E, R> =>
946+
effected.tap((value) => {
947+
console.log(`${message}: ${String(value)}`);
948+
});
949+
950+
expect(Effected.of(42).pipe(delay(100))).to(
951+
equal<Effected<Default<Effect<"delay", [ms: number], void>>, number>>,
952+
);
953+
954+
expect(Effected.of(42).pipe(delay(100), withLog("Result"))).to(
955+
equal<Effected<Default<Effect<"delay", [ms: number], void>>, number>>,
956+
);
957+
}
930958
});
931959

932960
test("Pipeline Syntax V.S. Generator Syntax", () => {

test/README.example.spec.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { expect, test, vi } from "vitest";
44

5-
import type { Effect, EffectFactory, Unresumable } from "../src";
5+
import type { Default, Effect, EffectFactory, Unresumable } from "../src";
66
import {
77
Effected,
88
UnhandledEffectError,
@@ -1365,7 +1365,7 @@ test("Parallel execution with `Effected.all`", async () => {
13651365
}
13661366
});
13671367

1368-
test("Effects without generators (Pipeline syntax)", () => {
1368+
test("Effects without generators (Pipeline syntax)", async () => {
13691369
{
13701370
const fib1 = (n: number): Effected<never, number> =>
13711371
effected(function* () {
@@ -1503,6 +1503,35 @@ test("Effects without generators (Pipeline syntax)", () => {
15031503
.runSync(),
15041504
).toBe("Welcome Alice! Using dark theme.");
15051505
}
1506+
1507+
// Test `.pipe(...fs)`
1508+
{
1509+
const delay =
1510+
(ms: number) =>
1511+
<E extends Effect, R>(
1512+
effected: Effected<E, R>,
1513+
): Effected<E | Default<Effect<"delay", [ms: number], void>>, R> =>
1514+
effect<[ms: number], void>()("delay", {
1515+
defaultHandler: ({ resume }, ms) => {
1516+
setTimeout(resume, ms);
1517+
},
1518+
})(ms).andThen(() => effected);
1519+
1520+
const withLog =
1521+
(message: string) =>
1522+
<E extends Effect, R>(effected: Effected<E, R>): Effected<E, R> =>
1523+
effected.tap((value) => {
1524+
console.log(`${message}: ${String(value)}`);
1525+
});
1526+
1527+
expect(await Effected.of(42).pipe(delay(100)).runAsync()).toBe(42);
1528+
1529+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
1530+
expect(await Effected.of(42).pipe(delay(100), withLog("Result")).runAsync()).toBe(42);
1531+
expect(logSpy).toHaveBeenCalledOnce();
1532+
expect(logSpy.mock.calls[0]![0]).toBe("Result: 42");
1533+
logSpy.mockRestore();
1534+
}
15061535
});
15071536

15081537
test("Pipeline Syntax VS Generator Syntax", async () => {

0 commit comments

Comments
 (0)