Skip to content

Commit 76fb3fe

Browse files
committed
✨ feat!: Rename Effected#map to andThen for improved API conciseness
1 parent 6235e6e commit 76fb3fe

File tree

7 files changed

+44
-41
lines changed

7 files changed

+44
-41
lines changed

README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -713,9 +713,9 @@ When you run `program.runSync()`, the output will be:
713713
"world"
714714
```
715715

716-
### Handling return values
716+
### Chaining effected programs with `.andThen()` and `.tap()`
717717

718-
Sometimes, you may want to transform a program’s return value. Let’s revisit the `safeDivide` example:
718+
Sometimes, you may want to transform a program’s return value, or chain another effected program after the current one. Let’s revisit the `safeDivide` example:
719719

720720
```typescript
721721
type Raise = Unresumable<Effect<"raise", [error: unknown], never>>;
@@ -737,18 +737,20 @@ const some = <T>(value: T): Option<T> => ({ kind: "some", value });
737737
const none: Option<never> = { kind: "none" };
738738
```
739739

740-
We want to transform the return value of `safeDivide` into an `Option` type: if `safeDivide` returns a value normally, we return `some(value)`, otherwise, we return `none` (if the raise effect is triggered). This can be achieved with the map method:
740+
We want to transform the return value of `safeDivide` into an `Option` type: if `safeDivide` returns a value normally, we return `some(value)`, otherwise, we return `none` (if the raise effect is triggered). This can be achieved with the `.andThen()` method:
741741

742742
```typescript
743743
const safeDivide2 = (a: number, b: number): Effected<never, Option<number>> =>
744744
safeDivide(a, b)
745-
.map((value) => some(value))
745+
.andThen((value) => some(value))
746746
.terminate("raise", () => none);
747747
```
748748

749749
Now, running `safeDivide2(1, 0).runSync()` will return `none`, while `safeDivide2(1, 2).runSync()` will return `some(0.5)`.
750750

751-
Besides `.map(handler)`, the `.tap(handler)` method offers a useful alternative when you want to execute side effects without altering the return value. Unlike `.map()`, `.tap()` ignores the return value of the handler function, ensuring the original value is preserved. This makes it ideal for operations like logging, where the action doesn’t modify the main data flow.
751+
Similar to most other methods in tinyeffect, `.andThen()` can also be used with a generator function to chain another effected program, which can be incredibly useful in many scenarios. We’ll see many more examples of this in the following sections.
752+
753+
Besides `.andThen(handler)`, the `.tap(handler)` method offers a useful alternative when you want to execute side effects without altering the return value. Unlike `.andThen()`, `.tap()` ignores the return value of the handler function, ensuring the original value is preserved. This makes it ideal for operations like logging, where the action doesn’t modify the main data flow.
752754

753755
For instance, you can use `.tap()` to simulate a `defer` effect similar to Go’s `defer` statement:
754756

@@ -883,7 +885,7 @@ const handleErrorAsResult = <R, E extends Effect, ErrorName extends string>(
883885
};
884886

885887
return effected
886-
.map((value) => ok(value))
888+
.andThen((value) => ok(value))
887889
.handle(isErrorEffect, ({ effect, terminate }, message) => {
888890
terminate(err({ error: effect.name.slice("error:".length), message }));
889891
});
@@ -912,7 +914,7 @@ The `handleErrorAsResult` helper function shown above is mainly for illustration
912914
```typescript
913915
const range3 = (start: number, stop: number) =>
914916
range(start, stop)
915-
.map((value) => ok(value))
917+
.andThen((value) => ok(value))
916918
.catchAll((error, message) => err({ error, message }));
917919
```
918920

@@ -1111,11 +1113,11 @@ const some = <T>(value: T): Option<T> => ({ kind: "some", value });
11111113
const none: Option<never> = { kind: "none" };
11121114
```
11131115

1114-
In previous examples, we used `.map()` and `.terminate()` to transform the return value of `safeDivide` to an `Option` type. Now, we can abstract this logic into a reusable handler:
1116+
In previous examples, we used `.andThen()` and `.terminate()` to transform the return value of `safeDivide` to an `Option` type. Now, we can abstract this logic into a reusable handler:
11151117

11161118
```typescript
11171119
const raiseOption = defineHandlerFor<Raise>().with((effected) =>
1118-
effected.map((value) => some(value)).terminate("raise", () => none),
1120+
effected.andThen((value) => some(value)).terminate("raise", () => none),
11191121
);
11201122

11211123
const safeDivide2 = (a: number, b: number) => safeDivide(a, b).with(raiseOption);
@@ -1154,14 +1156,14 @@ const fib2 = (n: number): Effected<never, number> => {
11541156
if (n <= 1) return Effected.of(n);
11551157
// Or use `Effected.from` with a getter:
11561158
// if (n <= 1) return Effected.from(() => n);
1157-
return fib2(n - 1).map((a) => fib2(n - 2).map((b) => a + b));
1159+
return fib2(n - 1).andThen((a) => fib2(n - 2).andThen((b) => a + b));
11581160
};
11591161
```
11601162

11611163
> [!NOTE]
11621164
>
11631165
> The above example is purely for illustrative purposes and _should not_ be used in practice. While it demonstrates how effects can be handled, it mimics the behavior of a simple fib function with unnecessary complexity and overhead, which could greatly degrade performance.
11641166
1165-
Understanding the definition of `fib2` may take some time, but it serves as an effective demonstration of working with effects without generators. The expression `fib2(n - 1).map((a) => fib2(n - 2).map((b) => a + b))` can be interpreted as follows: “After resolving `fib2(n - 1)`, assign the result to `a`, then resolve `fib2(n - 2)` and assign the result to `b`. Finally, return `a + b`.”
1167+
Understanding the definition of `fib2` may take some time, but it serves as an effective demonstration of working with effects without generators. The expression `fib2(n - 1).andThen((a) => fib2(n - 2).andThen((b) => a + b))` can be interpreted as follows: “After resolving `fib2(n - 1)`, assign the result to `a`, then resolve `fib2(n - 2)` and assign the result to `b`. Finally, return `a + b`.”
11661168

1167-
It’s important to note that the first `.map()` call behaves like a `flatMap` operation, as it takes a function that returns another `Effected` instance and “flattens” the result. However, in tinyeffect, the distinction between `map` and `flatMap` is not explicit — `.map()` will automatically flatten the result if it’s an `Effected` instance. This allows for seamless chaining of `.map()` calls as needed.
1169+
It’s important to note that the first `.andThen()` call behaves like a `flatMap` operation, as it takes a function that returns another `Effected` instance and “flattens” the result. However, in tinyeffect, the distinction between `map` and `flatMap` is not explicit — `.andThen()` will automatically flatten the result if it’s an `Effected` instance, just like `Promise.prototype.then()` in JavaScript. This allows for seamless chaining of `.andThen()` calls as needed.

src/README.example.proof.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ test("Handling return values", () => {
360360

361361
const safeDivide2 = (a: number, b: number) =>
362362
safeDivide(a, b)
363-
.map((value) => some(value))
363+
.andThen((value) => some(value))
364364
.terminate("raise", () => none);
365365

366366
expect(safeDivide2).to(equal<(a: number, b: number) => Effected<never, Option<number>>>);
@@ -470,7 +470,7 @@ test("Handling multiple effects in one handler", () => {
470470
};
471471

472472
return effected
473-
.map((value) => ok(value))
473+
.andThen((value) => ok(value))
474474
.handle(isErrorEffect, ({ effect, terminate }: any, message: any) => {
475475
terminate(err({ error: effect.name.slice("error:".length), message }));
476476
});
@@ -493,7 +493,7 @@ test("Handling multiple effects in one handler", () => {
493493

494494
const range4 = (start: number, stop: number) =>
495495
range(start, stop)
496-
.map((value) => ok(value))
496+
.andThen((value) => ok(value))
497497
.catchAll((error, ...args) =>
498498
err({ error, ...(args.length === 0 ? {} : { message: args[0] }) }),
499499
);
@@ -641,7 +641,7 @@ test("Abstracting handlers", () => {
641641
const none: Option<never> = { kind: "none" };
642642

643643
const raiseOption = defineHandlerFor<Raise>().with((effected) =>
644-
effected.map((value) => some(value)).terminate("raise", () => none),
644+
effected.andThen((value) => some(value)).terminate("raise", () => none),
645645
);
646646

647647
const safeDivide2 = (a: number, b: number) => safeDivide(a, b).with(raiseOption);

src/README.example.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ test("Handling return values", () => {
643643

644644
const safeDivide2 = (a: number, b: number): Effected<never, Option<number>> =>
645645
safeDivide(a, b)
646-
.map((value) => some(value))
646+
.andThen((value) => some(value))
647647
.terminate("raise", () => none);
648648

649649
expect(safeDivide2(1, 0).runSync()).toEqual(none);
@@ -753,7 +753,7 @@ test("Handling multiple effects in one handler", () => {
753753
};
754754

755755
return effected
756-
.map((value) => ok(value))
756+
.andThen((value) => ok(value))
757757
.handle(isErrorEffect, ({ effect, terminate }: any, message: any) => {
758758
terminate(err({ error: effect.name.slice("error:".length), message }));
759759
});
@@ -843,7 +843,7 @@ test("Handling multiple effects in one handler", () => {
843843

844844
const range4 = (start: number, stop: number) =>
845845
range(start, stop)
846-
.map((value) => ok(value))
846+
.andThen((value) => ok(value))
847847
.catchAll((error, message) => err({ error, message }));
848848

849849
expect(
@@ -1157,7 +1157,7 @@ test("Abstracting handlers", () => {
11571157
const none: Option<never> = { kind: "none" };
11581158

11591159
const raiseOption = defineHandlerFor<Raise>().with((effected) =>
1160-
effected.map((value) => some(value)).terminate("raise", () => none),
1160+
effected.andThen((value) => some(value)).terminate("raise", () => none),
11611161
);
11621162

11631163
const safeDivide2 = (a: number, b: number) => safeDivide(a, b).with(raiseOption);
@@ -1176,12 +1176,12 @@ test("Effects without generators", () => {
11761176

11771177
const fib2 = (n: number): Effected<never, number> => {
11781178
if (n <= 1) return Effected.of(n);
1179-
return fib2(n - 1).map((a) => fib2(n - 2).map((b) => a + b));
1179+
return fib2(n - 1).andThen((a) => fib2(n - 2).andThen((b) => a + b));
11801180
};
11811181

11821182
const fib3 = (n: number): Effected<never, number> => {
11831183
if (n <= 1) return Effected.from(() => n);
1184-
return fib2(n - 1).map((a) => fib2(n - 2).map((b) => a + b));
1184+
return fib2(n - 1).andThen((a) => fib2(n - 2).andThen((b) => a + b));
11851185
};
11861186

11871187
expect(fib1(10).runSync()).toBe(55);

src/effected.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -327,20 +327,20 @@ describe("effected", () => {
327327

328328
expect(
329329
raise42(42)
330-
.map(some)
330+
.andThen(some)
331331
.catch("myError", () => none)
332332
.runSync(),
333333
).toEqual(none);
334334
expect(
335335
raise42(21)
336-
.map(some)
336+
.andThen(some)
337337
.catch("myError", () => none)
338338
.runSync(),
339339
).toEqual(some(21));
340340

341341
expect(
342342
raise42(42)
343-
.map(function* () {
343+
.andThen(function* () {
344344
return yield* random();
345345
})
346346
.catchAll((name, msg) => ({ name, msg }))
@@ -349,7 +349,7 @@ describe("effected", () => {
349349
).toEqual({ name: "myError", msg: "42 is not allowed" });
350350
expect(
351351
raise42(21)
352-
.map(function* () {
352+
.andThen(function* () {
353353
return yield* random();
354354
})
355355
.catch("myError", () => none)
@@ -359,7 +359,7 @@ describe("effected", () => {
359359

360360
expect(
361361
await raise42(42)
362-
.map(function* (n) {
362+
.andThen(function* (n) {
363363
return yield* effectify(new Promise((resolve) => setTimeout(() => resolve(some(n)), 0)));
364364
})
365365
.catchAll(function* (name, msg) {
@@ -371,7 +371,7 @@ describe("effected", () => {
371371
).toEqual({ name: "myError", msg: "42 is not allowed" });
372372
expect(
373373
await raise42(21)
374-
.map(function* (n) {
374+
.andThen(function* (n) {
375375
return yield* effectify(new Promise((resolve) => setTimeout(() => resolve(some(n)), 0)));
376376
})
377377
.catch("myError", () => none)
@@ -388,7 +388,7 @@ describe("effected", () => {
388388
const none: Option<never> = { type: "None" };
389389

390390
const raiseOption = defineHandlerFor<Raise>().with((effected) =>
391-
effected.map(some).terminate("raise", () => none),
391+
effected.andThen(some).terminate("raise", () => none),
392392
);
393393

394394
const raise42 = (n: number) =>

src/effected.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ type DependencyName<E extends Effect> =
180180
* const none: Option<never> = { kind: "none" };
181181
*
182182
* const raiseOption = defineHandlerFor<Raise>().with((effected) =>
183-
* effected.map((value) => some(value)).terminate("raise", () => none),
183+
* effected.andThen((value) => some(value)).terminate("raise", () => none),
184184
* );
185185
*
186186
* const safeDivide2 = (a: number, b: number) => safeDivide(a, b).with(raiseOption);
@@ -547,15 +547,16 @@ export class Effected<out E extends Effect, out R> implements Iterable<E, R, unk
547547
}
548548

549549
/**
550-
* Map the return value of the effected program.
551-
* @param handler The function to map the return value.
550+
* Chains another function or effected program after the current one, where the chained function
551+
* or effected program will receive the return value of the current one.
552+
* @param handler The function or effected program to chain after the current one.
552553
* @returns
553554
*/
554-
map<S, F extends Effect = never>(
555+
andThen<S, F extends Effect = never>(
555556
handler: (value: R) => Generator<F, S, unknown> | Effected<F, S>,
556557
): Effected<E | F, S>;
557-
map<S>(handler: (value: R) => S): Effected<E, S>;
558-
map(handler: (value: R) => unknown): Effected<Effect, unknown> {
558+
andThen<S>(handler: (value: R) => S): Effected<E, S>;
559+
andThen(handler: (value: R) => unknown): Effected<Effect, unknown> {
559560
const iterator = this[Symbol.iterator]();
560561

561562
return effected(() => {
@@ -587,7 +588,7 @@ export class Effected<out E extends Effect, out R> implements Iterable<E, R, unk
587588
tap<F extends Effect = never>(
588589
handler: (value: R) => void | Generator<F, void, unknown> | Effected<F, void>,
589590
): Effected<E | F, R> {
590-
return this.map((value) => {
591+
return this.andThen((value) => {
591592
const it = handler(value);
592593
if (!(it instanceof Effected) && !isGenerator(it)) return value;
593594
const iterator = it[Symbol.iterator]();
@@ -824,10 +825,10 @@ interface EffectedDraft<
824825
handler: E extends Effect<Name, infer Payloads> ? (...payloads: Payloads) => T : never,
825826
): EffectedDraft<P, Exclude<E, Effect<Name>>, R | T>;
826827

827-
map<S, F extends Effect = never>(
828+
andThen<S, F extends Effect = never>(
828829
handler: (value: R) => Generator<F, S, unknown> | Effected<F, S>,
829830
): EffectedDraft<P, E | F, S>;
830-
map<S>(handler: (value: R) => S): EffectedDraft<P, E, S>;
831+
andThen<S>(handler: (value: R) => S): EffectedDraft<P, E, S>;
831832

832833
tap<F extends Effect = never>(
833834
handler: (value: R) => void | Generator<F, void, unknown> | Effected<F, void>,

src/fib.bench.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const fibGenT = (n: number): Effected<never, number> =>
3232

3333
const fibPipeT = (n: number): Effected<never, number> => {
3434
if (n <= 1) return Effected.of(n);
35-
return fibPipeT(n - 1).map((a) => fibPipeT(n - 2).map((b) => a + b));
35+
return fibPipeT(n - 1).andThen((a) => fibPipeT(n - 2).andThen((b) => a + b));
3636
};
3737

3838
/* Effect */

src/koka-samples.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const just = <T>(value: T): Maybe<T> => ({ _tag: "Just", value });
6565
const nothing: Maybe<never> = { _tag: "Nothing" };
6666

6767
const raiseMaybe = defineHandlerFor<Raise>().with((effected) =>
68-
effected.map((r) => just(r)).terminate("raise", () => nothing),
68+
effected.andThen((r) => just(r)).terminate("raise", () => nothing),
6969
);
7070

7171
test("3.2.3. Polymorphic effects", () => {
@@ -231,7 +231,7 @@ const pState = <T>(init: T) =>
231231
defineHandlerFor<State<T>>().with((effected) => {
232232
let st = init;
233233
return effected
234-
.map((x) => [x, st] as const)
234+
.andThen((x) => [x, st] as const)
235235
.resume("state.get", () => st)
236236
.resume("state.set", (x) => {
237237
st = x;

0 commit comments

Comments
 (0)