Skip to content

Commit f97c801

Browse files
committed
✨ feat: effect and effectify returns Effected instead of generator
1. `effect` and its variants now returns an effected function instead of a generator function for consistency. 2. `effectify` now returns an `Effected` instance instead of a generator for consistency.
1 parent ccf4cb3 commit f97c801

File tree

8 files changed

+160
-124
lines changed

8 files changed

+160
-124
lines changed

README.md

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -397,44 +397,12 @@ export interface Effect<
397397

398398
At runtime, only `name` and `payloads` are present, while `__returnType` serves purely at the type level to infer the effect’s return type.
399399

400-
Using `yield*` with a factory function created by `effect` (or its variants) yields an `Effect` object. The `effect` function itself can be simplified as follows:
401-
402-
```typescript
403-
function effect<Name extends string | symbol>(
404-
name: Name,
405-
): <Payloads extends unknown[], R>(
406-
...payloads: Payloads
407-
) => Generator<Effect<Name, Payloads, R>, R, unknown> {
408-
return function* (...payloads) {
409-
return yield { name: name, payloads };
410-
};
411-
}
412-
413-
function error<Name extends string>(
414-
name: Name,
415-
): <Payloads extends unknown[]>(
416-
message?: string,
417-
) => Generator<Unresumable<Effect<`error:${Name}`, Payloads, never>>, never, unknown> {
418-
return function* (...payloads) {
419-
return yield { name: `error:${name}`, payloads, resumable: false };
420-
};
421-
}
422-
423-
function dependency<Name extends string>(
424-
name: Name,
425-
): <R>() => Generator<Effect<`dependency:${Name}`, [], R>, R, unknown> {
426-
return function* () {
427-
return yield { name: `dependency:${name}`, payloads: [] };
428-
};
429-
}
430-
```
431-
432-
To keep things simple, let’s remove the type signatures and focus on the structure:
400+
Using `yield*` with a factory function created by `effect` (or its variants) yields an `Effect` object. To understand how it works, let’s take a look at a simplified version of the `effect` function (and its variants) in JavaScript:
433401

434402
```typescript
435403
function effect(name) {
436404
return function* (...payloads) {
437-
return yield { name: name, payloads };
405+
return yield { name, payloads };
438406
};
439407
}
440408

@@ -451,6 +419,8 @@ function dependency(name) {
451419
}
452420
```
453421

422+
While the actual implementation of `effect` is more complex and returns a factory function that produces an `Effected` instance (instead of a generator function), the fundamental concept remains the same.
423+
454424
The mechanism of `.runSync()` and `.runAsync()` is also straightforward. These methods iterate through the generator function, and when encountering an `Effect` object, invoke the corresponding handler registered by `.handle()`. They then either resume or terminate the generator with the value passed to `resume` or `terminate`. The actual implementation is more complex, but the concept remains consistent.
455425

456426
> [!NOTE]
@@ -539,7 +509,7 @@ When you hover over the `raise` variable, you’ll see its `Effect` type is wrap
539509
```typescript
540510
const raise: (
541511
error: unknown,
542-
) => Generator<Unresumable<Effect<"raise", [error: unknown], never>>, never, unknown>;
512+
) => Effected<Unresumable<Effect<"raise", [error: unknown], never>>, never>;
543513
```
544514

545515
Attempting to handle an unresumable effect with `.resume()` will result in a TypeScript error:
@@ -681,7 +651,7 @@ type Iterate<T> = Effect<"iterate", [value: T], void>;
681651
const iterate = <T>(value: T) => effect("iterate")<[value: T], void>(value);
682652
```
683653

684-
This might look complex at first, but it simply wraps the `effect` function to make it generic. `effect("iterate")` returns a generic generator function, and we pass type arguments to specialize it, then call the function with a value to yield an Effect object.
654+
This might look complex at first, but it simply wraps the `effect` function to make it generic. `effect("iterate")` returns a generic factory function that produces an `Effected` instance, and we pass type arguments to specialize it, then call the function with a value to yield an `Effect` object.
685655

686656
### Handling effects with another effected program
687657

@@ -691,7 +661,7 @@ For example, consider the following program:
691661

692662
```typescript
693663
type Ask<T> = Effect<"ask", [], T>;
694-
const ask = <T>(): Generator<Ask<T>, T, unknown> => effect("ask")();
664+
const ask = <T>(): Effected<Ask<T>, T> => effect("ask")();
695665

696666
const double = (): Effected<Ask<number>, number> =>
697667
effected(function* () {
@@ -1015,9 +985,8 @@ To group these together, we could define them as:
1015985
```typescript
1016986
type State<T> = Effect<"state.get", [], T> | Effect<"state.set", [value: T], void>;
1017987
const state = {
1018-
get: <T>(): Generator<State<T>, T, unknown> => effect("state.get")<[], T>(),
1019-
set: <T>(value: T): Generator<State<T>, void, unknown> =>
1020-
effect("state.set")<[value: T], void>(value),
988+
get: <T>(): Effected<State<T>, T> => effect("state.get")<[], T>(),
989+
set: <T>(value: T): Effected<State<T>, void> => effect("state.set")<[value: T], void>(value),
1021990
};
1022991
```
1023992

src/README.example.proof.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,7 @@ test("The `Effect` type > Unresumable effects", () => {
206206

207207
expect(raise).to(
208208
equal<
209-
(
210-
error: unknown,
211-
) => Generator<Unresumable<Effect<"raise", [error: unknown], never>>, never, unknown>
209+
(error: unknown) => Effected<Unresumable<Effect<"raise", [error: unknown], never>>, never>
212210
>,
213211
);
214212

@@ -296,7 +294,7 @@ test("A deep dive into `resume` and `terminate`", () => {
296294
test("Handling effects with another effected program", () => {
297295
{
298296
type Ask<T> = Effect<"ask", [], T>;
299-
const ask = <T>(): Generator<Ask<T>, T, unknown> => effect("ask")();
297+
const ask = <T>(): Effected<Ask<T>, T> => effect("ask")();
300298

301299
const double = (): Effected<Ask<number>, number> =>
302300
effected(function* () {
@@ -567,9 +565,8 @@ test("Abstracting handlers", () => {
567565
{
568566
type State<T> = Effect<"state.get", [], T> | Effect<"state.set", [value: T], void>;
569567
const state = {
570-
get: <T>(): Generator<State<T>, T, unknown> => effect("state.get")<[], T>(),
571-
set: <T>(value: T): Generator<State<T>, void, unknown> =>
572-
effect("state.set")<[value: T], void>(value),
568+
get: <T>(): Effected<State<T>, T> => effect("state.get")<[], T>(),
569+
set: <T>(value: T): Effected<State<T>, void> => effect("state.set")<[value: T], void>(value),
573570
};
574571

575572
const sumDown = (sum: number = 0): Effected<State<number>, number> =>

src/README.example.spec.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ test("A deep dive into `resume` and `terminate`", () => {
567567
test("Handling effects with another effected program", () => {
568568
{
569569
type Ask<T> = Effect<"ask", [], T>;
570-
const ask = <T>(): Generator<Ask<T>, T, unknown> => effect("ask")();
570+
const ask = <T>(): Effected<Ask<T>, T> => effect("ask")();
571571

572572
const double = () =>
573573
effected(function* () {
@@ -1051,9 +1051,8 @@ test("Abstracting handlers", () => {
10511051
{
10521052
type State<T> = Effect<"state.get", [], T> | Effect<"state.set", [value: T], void>;
10531053
const state = {
1054-
get: <T>(): Generator<State<T>, T, unknown> => effect("state.get")<[], T>(),
1055-
set: <T>(value: T): Generator<State<T>, void, unknown> =>
1056-
effect("state.set")<[value: T], void>(value),
1054+
get: <T>(): Effected<State<T>, T> => effect("state.get")<[], T>(),
1055+
set: <T>(value: T): Effected<State<T>, void> => effect("state.set")<[value: T], void>(value),
10571056
};
10581057

10591058
const sumDown = (sum: number = 0): Effected<State<number>, number> =>

src/effected.proof.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,16 @@ const log = effect("log")<unknown[], void>;
1313
const raise = effect("raise", { resumable: false })<[error: unknown], never>;
1414

1515
describe("effect", () => {
16-
it("should create a generator function yielding a single `Effect`", () => {
17-
expect(add42).to(
18-
equal<(n: number) => Generator<Effect<"add42", [n: number], number>, number, unknown>>,
19-
);
20-
expect(now).to(equal<() => Generator<Effect<"now", [], Date>, Date, unknown>>);
21-
expect(log).to(
22-
equal<(...args: unknown[]) => Generator<Effect<"log", unknown[], void>, void, unknown>>,
23-
);
16+
it("should create a function that returns an `Effected` instance which yields a single `Effect`", () => {
17+
expect(add42).to(equal<(n: number) => Effected<Effect<"add42", [n: number], number>, number>>);
18+
expect(now).to(equal<() => Effected<Effect<"now", [], Date>, Date>>);
19+
expect(log).to(equal<(...args: unknown[]) => Effected<Effect<"log", unknown[], void>, void>>);
2420
});
2521

2622
it("should create unresumable effects", () => {
2723
expect(raise).to(
2824
equal<
29-
(
30-
error: unknown,
31-
) => Generator<Unresumable<Effect<"raise", [error: unknown], never>>, never, unknown>
25+
(error: unknown) => Effected<Unresumable<Effect<"raise", [error: unknown], never>>, never>
3226
>,
3327
);
3428
});
@@ -46,15 +40,14 @@ describe("effect", () => {
4640
const typeError = error("type");
4741

4842
describe("error", () => {
49-
it("should create a generator function yielding an unresumable error effect", () => {
43+
it("should create a function that returns an `Effected` instance which yields a single `Effect.Error`", () => {
5044
expect(typeError).to(
5145
equal<
5246
(
5347
message?: string,
54-
) => Generator<
48+
) => Effected<
5549
Unresumable<Effect<"error:type", [message?: string | undefined], never>>,
56-
never,
57-
unknown
50+
never
5851
>
5952
>,
6053
);
@@ -71,10 +64,8 @@ describe("error", () => {
7164
const askNumber = dependency("number")<number>;
7265

7366
describe("dependency", () => {
74-
it("should create a generator function yielding a single `Effect.Dependency`", () => {
75-
expect(askNumber).to(
76-
equal<() => Generator<Effect.Dependency<"number", number>, number, unknown>>,
77-
);
67+
it("should create a function that returns an `Effected` instance which yields a single `Effect.Dependency`", () => {
68+
expect(askNumber).to(equal<() => Effected<Effect.Dependency<"number", number>, number>>);
7869
});
7970

8071
it("should be inferred as an `Effect` type by the `InferEffect` utility type", () => {

src/effected.spec.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,38 +23,41 @@ const log = effect("log")<unknown[], void>;
2323
const raise = effect("raise", { resumable: false })<[error: unknown], never>;
2424

2525
describe("effect", () => {
26-
it("should create a generator function yielding a single `Effect`", () => {
26+
it("should create a function that returns an `Effected` instance which yields a single `Effect`", () => {
2727
{
28-
const it = add42(42);
28+
const it = add42(42)[Symbol.iterator]();
2929
const result = it.next();
3030
expect(result).toEqual({ value: { name: "add42", payloads: [42] }, done: false });
3131
expect(result.value).toBeInstanceOf(Effect);
3232
expect(it.next(84)).toEqual({ value: 84, done: true });
33+
expect(it.next()).toEqual({ done: true });
3334
}
3435

3536
{
36-
const it = now();
37+
const it = now()[Symbol.iterator]();
3738
const result = it.next();
3839
expect(result).toEqual({ value: { name: "dependency:now", payloads: [] }, done: false });
3940
expect(result.value).toBeInstanceOf(Effect);
4041
const time = new Date();
4142
expect(it.next(time)).toEqual({ value: time, done: true });
43+
expect(it.next()).toEqual({ done: true });
4244
}
4345

4446
{
45-
const it = log("hello", "world", 42);
47+
const it = log("hello", "world", 42)[Symbol.iterator]();
4648
const result = it.next();
4749
expect(result).toEqual({
4850
value: { name: "log", payloads: ["hello", "world", 42] },
4951
done: false,
5052
});
5153
expect(result.value).toBeInstanceOf(Effect);
5254
expect(it.next()).toEqual({ value: undefined, done: true });
55+
expect(it.next()).toEqual({ done: true });
5356
}
5457
});
5558

5659
it("should create unresumable effects", () => {
57-
const it = raise("error");
60+
const it = raise("error")[Symbol.iterator]();
5861
const result = it.next();
5962
expect(result).toEqual({
6063
value: { name: "raise", payloads: ["error"], resumable: false },
@@ -67,8 +70,8 @@ describe("effect", () => {
6770
const typeError = error("type");
6871

6972
describe("error", () => {
70-
it("should create a generator function yielding an unresumable error effect", () => {
71-
const it = typeError("type error");
73+
it("should create a function that returns an `Effected` instance which yields a single `Effect.Error`", () => {
74+
const it = typeError("type error")[Symbol.iterator]();
7275
const result = it.next();
7376
expect(result).toEqual({
7477
value: { name: "error:type", payloads: ["type error"], resumable: false },
@@ -81,15 +84,31 @@ describe("error", () => {
8184
const askNumber = dependency("number")<number>;
8285

8386
describe("dependency", () => {
84-
it("should create a generator function yielding a single `Effect.Dependency`", () => {
85-
const it = askNumber();
87+
it("should create a function that returns an `Effected` instance which yields a single `Effect.Dependency`", () => {
88+
const it = askNumber()[Symbol.iterator]();
8689
const result = it.next();
8790
expect(result).toEqual({
8891
value: { name: "dependency:number", payloads: [] },
8992
done: false,
9093
});
9194
expect(result.value).toBeInstanceOf(Effect);
9295
expect(it.next(42)).toEqual({ value: 42, done: true });
96+
expect(it.next()).toEqual({ done: true });
97+
});
98+
});
99+
100+
describe("effectify", () => {
101+
it("should transform a promise into an `Effected` instance", () => {
102+
const effected = effectify(Promise.resolve(42));
103+
expect(effected).toBeInstanceOf(Effected);
104+
const it = effected[Symbol.iterator]();
105+
const result = it.next();
106+
expect(result).toEqual({
107+
value: { _effectAsync: true, onComplete: expect.any(Function) },
108+
done: false,
109+
});
110+
expect(it.next(42)).toEqual({ value: 42, done: true });
111+
expect(it.next()).toEqual({ done: true });
93112
});
94113
});
95114

0 commit comments

Comments
 (0)