-
Notifications
You must be signed in to change notification settings - Fork 32
/
004-generators.ts
149 lines (135 loc) · 4.47 KB
/
004-generators.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import { pipe, Effect, Context } from "effect";
/* Callback hell.
*
* If you have written any JavaScript you have seen it. Sadly, even fp-ts code
* or other code written in a functional style is not immune to it, even inside
* high quality codebases.
*/
class CustomRandom extends Context.Tag("CustomRandom")<
CustomRandom,
{ readonly next: () => number }
>() {}
class Foo extends Context.Tag("Foo")<Foo, { readonly foo: number }>() {}
class Bar extends Context.Tag("Bar")<Bar, { readonly bar: number }>() {}
/*
* Effect would be very similar - the main issue is any time you have a new
* dependency in your code, you end up using flatMap and the indentation grows.
*/
export const hell = Effect.flatMap(CustomRandom, random =>
Effect.flatMap(Foo, foo =>
Effect.flatMap(Bar, bar =>
Effect.sync(() => {
console.log("please stop!!!", random.next, foo.foo, bar.bar);
return "hell" as const;
}),
),
),
);
/*
* For an example so trivial we can actually still get away with the pipe based
* API using the "all" function built in into Effect.
*/
export const tuple = pipe(
Effect.all([CustomRandom, Foo, Bar]),
Effect.flatMap(([random, foo, bar]) =>
Effect.sync(() => {
console.log("not as bad!", random.next(), foo.foo, bar.bar);
return "tuple" as const;
}),
),
);
// Effect.all preserves the shape of it's argument
export const tuple2 = pipe(
Effect.all({ random: CustomRandom, foo: Foo, bar: Bar }),
Effect.flatMap(({ random, foo, bar }) =>
Effect.sync(() => {
console.log("not as bad!", random.next(), foo.foo, bar.bar);
return "tuple" as const;
}),
),
);
/*
* But you would still end up with messy code in real application code, not to
* mention testing code!
*
* To address this issue, Effect has an API that uses generators to avoid
* callback hell.
*/
export const generator = Effect.gen(function* () {
/* NOTE: Unfortunately Effects must be wrapped in this $ function because of
* shortcomings in the TypeScript language. Someday the TypeScript team might
* improve how typings in generators work and Effect could drop this $ as a
* result.
*/
const random = yield* CustomRandom;
const foo = yield* Foo;
const bar = yield* Bar;
console.log("this is pretty cool!", random.next(), foo.foo, bar.bar);
return "generator" as const;
});
/* A legit question would be: How do you error out of a generator function?
* Just yield a failing Effect
*/
export const generatorerr = Effect.gen(function* () {
const foo = yield* Foo;
const bar = yield* Bar;
const random = (yield* CustomRandom).next();
if (random > 0.5) {
// Whenever this code block is reached, it will exact this generator
yield* Effect.fail("bad random" as const);
}
console.log("this is pretty cool!", random, foo.foo, bar.bar);
return "generator" as const;
});
/*
* Another option for avoiding callback hell is "Do notation".
* This lets you bind effects/values to names when using pipe without
* introducing more nesting.
*
* NOTE: when working with Effect streams, generators don't work. In those
* instances the Do notation the only option.
*/
export const doNotation = pipe(
Effect.Do,
Effect.bind("random", () => CustomRandom),
Effect.bind("foo", () => Foo),
Effect.bind("bar", () => Bar),
Effect.flatMap(({ random, foo, bar }) =>
Effect.sync(() =>
console.log("this is pretty cool!", random.next(), foo.foo, bar.bar),
),
),
);
/*
* TLDR: With generators you can write Effect code that looks imperative!
* It's equivalent to what ZIO does in Scala with for comprehensions.
*
* Admittedly, `gen(function* ($) {` and `yield* $(` add quite a bit of noise,
* but considering the limitations of JavaScript and TypeScript, it's quite
* amazing that this is possible at all.
*
* Code snippets are advised to write out the `gen(function *($)` and `yield* $()`
* boilerplate. For reference, I setup mine like this:
{
"Gen Function $": {
"prefix": "gen$",
"body": ["function* ($) {\n\t$0\n}"],
"description": "Generator function with $ input"
},
"Gen Function $ (wrapped)": {
"prefix": "egen$",
"body": ["Effect.gen(function* ($) {\n\t$0\n})"],
"description": "Generator function with $ input"
},
"Gen Yield $": {
"prefix": "yield$",
"body": ["yield* $($0)"],
"description": "Yield generator calling $()"
},
"Gen Yield $ (const)": {
"prefix": "cyield$",
"body": ["const $1 = yield* $($0)"],
"description": "Yield generator calling $()"
}
}
*/