Skip to content

Commit 59e3a46

Browse files
committed
✨ feat: Add Effected.of/from
1 parent 999e7d3 commit 59e3a46

File tree

5 files changed

+114
-3
lines changed

5 files changed

+114
-3
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,3 +1132,34 @@ For more complex cases where `defineHandlerFor` isn’t sufficient, you can stil
11321132
const range = (start: number, stop: number) => range(start, stop).with(handleErrorAsResult);
11331133
const range = (start: number, stop: number) => handleErrorAsResult(range(start, stop));
11341134
```
1135+
1136+
### Effects without generators
1137+
1138+
The fundamental logic of tinyeffect is _not_ dependent on generators. An effected program (represented as an `Effected` instance) is essentially an iterable object that implements a `[Symbol.iterator](): Iterator<Effect>` method. Although using the `effected` helper function in conjunction with a generator allows you to write more imperative-style code with `yield*` to manage effects, this is not the only way to handle them.
1139+
1140+
In fact, `effected` can accept any function that returns an iterator of effects — specifically, any function that returns an object implementing a `.next()` method that outputs objects with `value` and `done` properties.
1141+
1142+
It is not even necessary to use `effected` to construct an effected program. You can also create them using `Effected.of()` or `Effected.from()`. Here are two equivalent examples:
1143+
1144+
```typescript
1145+
const fib1 = (n: number): Effected<never, number> =>
1146+
effected(function* () {
1147+
if (n <= 1) return n;
1148+
return (yield* fib1(n - 1)) + (yield* fib1(n - 2));
1149+
});
1150+
1151+
const fib2 = (n: number): Effected<never, number> => {
1152+
if (n <= 1) return Effected.of(n);
1153+
// Or use `Effected.from` with a getter:
1154+
// if (n <= 1) return Effected.from(() => n);
1155+
return fib2(n - 1).map((a) => fib2(n - 2).map((b) => a + b));
1156+
};
1157+
```
1158+
1159+
> [!NOTE]
1160+
>
1161+
> 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.
1162+
1163+
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`.”
1164+
1165+
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.

src/README.example.proof.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
import { equal, expect, extend, test, error as triggerError } from "typroof";
55

6-
import { defineHandlerFor, dependency, effect, effected, effectify, error } from ".";
6+
import { Effected, defineHandlerFor, dependency, effect, effected, effectify, error } from ".";
77

8-
import type { Effect, EffectFactory, Effected, InferEffect, UnhandledEffect, Unresumable } from ".";
8+
import type { Effect, EffectFactory, InferEffect, UnhandledEffect, Unresumable } from ".";
99

1010
type User = { id: number; name: string; role: "admin" | "user" };
1111

@@ -616,3 +616,11 @@ test("Abstracting handlers", () => {
616616
expect(safeDivide2).to(equal<(a: number, b: number) => Effected<never, Option<number>>>);
617617
}
618618
});
619+
620+
test("Effects without generators", () => {
621+
expect(Effected.of(42)).not.to(triggerError);
622+
expect(Effected.of(42)).to(equal<Effected<never, number>>);
623+
624+
expect(Effected.from(() => 42)).not.to(triggerError);
625+
expect(Effected.from(() => 42)).to(equal<Effected<never, number>>);
626+
});

src/README.example.spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { expect, test, vi } from "vitest";
55

66
import {
7+
Effected,
78
UnhandledEffectError,
89
defineHandlerFor,
910
dependency,
@@ -13,7 +14,7 @@ import {
1314
error,
1415
} from ".";
1516

16-
import type { Effect, EffectFactory, Effected, Unresumable } from ".";
17+
import type { Effect, EffectFactory, Unresumable } from ".";
1718

1819
test("banner", async () => {
1920
type User = { id: number; name: string; role: "admin" | "user" };
@@ -1108,3 +1109,25 @@ test("Abstracting handlers", () => {
11081109
expect(safeDivide2(1, 2).runSync()).toEqual(some(0.5));
11091110
}
11101111
});
1112+
1113+
test("Effects without generators", () => {
1114+
const fib1 = (n: number): Effected<never, number> =>
1115+
effected(function* () {
1116+
if (n <= 1) return n;
1117+
return (yield* fib1(n - 1)) + (yield* fib1(n - 2));
1118+
});
1119+
1120+
const fib2 = (n: number): Effected<never, number> => {
1121+
if (n <= 1) return Effected.of(n);
1122+
return fib2(n - 1).map((a) => fib2(n - 2).map((b) => a + b));
1123+
};
1124+
1125+
const fib3 = (n: number): Effected<never, number> => {
1126+
if (n <= 1) return Effected.from(() => n);
1127+
return fib2(n - 1).map((a) => fib2(n - 2).map((b) => a + b));
1128+
};
1129+
1130+
expect(fib1(10).runSync()).toBe(55);
1131+
expect(fib2(10).runSync()).toBe(55);
1132+
expect(fib3(10).runSync()).toBe(55);
1133+
});

src/effected.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,3 +736,29 @@ describe("Effected#catchAllAndThrow", () => {
736736
expect(thrown).toBe(true);
737737
});
738738
});
739+
740+
describe("Effected.of", () => {
741+
it("should create an effected program with a single value", () => {
742+
const program = Effected.of(42);
743+
expect(program.runSync()).toBe(42);
744+
});
745+
});
746+
747+
describe("Effected.from", () => {
748+
it("should create an effected program from a generator function", () => {
749+
const program = Effected.from(() => 42);
750+
expect(program.runSync()).toBe(42);
751+
});
752+
753+
it("should only run the getter once", () => {
754+
let count = 0;
755+
const program = Effected.from(() => {
756+
count++;
757+
return 42;
758+
});
759+
expect(program.runSync()).toBe(42);
760+
expect(count).toBe(1);
761+
expect(program.runSync()).toBe(42);
762+
expect(count).toBe(2);
763+
});
764+
});

src/effected.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,29 @@ export class Effected<out E extends Effect, out R> implements Iterable<E, R, unk
201201
this.runAsyncUnsafe = () => runAsync(this as never);
202202
}
203203

204+
/**
205+
* Create an {@link Effected} instance that just returns the value.
206+
* @param value The value to return.
207+
* @returns
208+
*
209+
* @since 0.1.2
210+
*/
211+
static of<R>(value: R): Effected<never, R> {
212+
return effected(() => ({ next: () => ({ done: true, value }) })) as Effected<never, R>;
213+
}
214+
215+
/**
216+
* Create an {@link Effected} instance that just returns the value from a getter.
217+
* @param getter The getter to get the value.
218+
* @returns
219+
*/
220+
static from<R>(getter: () => R): Effected<never, R> {
221+
return effected(() => ({ next: () => ({ done: true, value: getter() }) })) as Effected<
222+
never,
223+
R
224+
>;
225+
}
226+
204227
/**
205228
* Handle an effect with a handler.
206229
*

0 commit comments

Comments
 (0)