You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
const range = (start:number, stop:number) =>handleErrorAsResult(range(start, stop));
1136
1136
```
1137
1137
1138
-
### Effects without generators (Pipeline syntax)
1138
+
### Parallel execution with `Effected.all`
1139
1139
1140
-
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.
1140
+
When working with asynchronous effects, you frequently need to combine multiple operations. While generator syntax excels at expressing sequential code, it doesn’t provide a native way to run effects in parallel using`yield*`. To address this, tinyeffect offers two complementary methods for handling multiple effected programs:
1141
1141
1142
-
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.
1142
+
-`Effected.all()`: Executes effected programs in parallel (concurrently)
1143
+
-`Effected.allSeq()`: Executes effected programs sequentially (one after another), equivalent to running them individually with `yield*`.
1143
1144
1144
-
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:
1145
+
It’s worth noting that when all effected programs are synchronous, `Effected.all()`and`Effected.allSeq()` produce identical results. The difference becomes significant when dealing with time-consuming operations:
1145
1146
1146
1147
```typescript
1147
-
constfib1= (n:number):Effected<never, number>=>
1148
+
constfetchUserData= (userId:number) =>
1148
1149
effected(function* () {
1149
-
if (n<=1) returnn;
1150
-
return (yield*fib1(n-1)) + (yield*fib1(n-2));
1150
+
yield*log(`Fetching user ${userId}`);
1151
+
const data =yield*httpGet(`/api/users/${userId}`);
// Sequential execution - total time is the sum of all operations
1156
+
const sequentialFetch =Effected.allSeq([fetchUserData(1), fetchUserData(2), fetchUserData(3)]); // Takes ~300ms if each fetch takes ~100ms
1157
+
1158
+
// Parallel execution - total time is close to the slowest operation
1159
+
const parallelFetch =Effected.all([fetchUserData(1), fetchUserData(2), fetchUserData(3)]); // Takes ~100ms because all fetches run concurrently
1159
1160
```
1160
1161
1161
-
> [!NOTE]
1162
-
>
1163
-
> 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
+
Both methods accept either an iterable of effected programs or an object with named effected programs:
1164
1163
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).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`.”
You can compare `Effected` with `Promise` in JavaScript. Just like `Promise.prototype.then(handler)` allows you to chain multiple promises together, `Effected.prototype.andThen(handler)` allows you to chain multiple effected programs together. If a handler returns a generator or another effected program, it will be automatically flattened, similar to how `Promise.prototype.then()` works in JavaScript.
// Total execution time will be ~150ms (the slowest task)
1207
+
```
1168
1208
1169
-
Another way to think of `Effected` is as a container for a delayed computation (or _monad_, if you come from a functional programming background). The `Effected` instance itself doesn't perform any computation; it only represents a sequence of effects that will be executed when you call `.runSync()` or `.runAsync()`.
1209
+
When should you choose sequential execution with `Effected.allSeq`? Consider using it when:
1170
1210
1171
-
Actually, tinyeffect provides two more methods, `.map()` and `.flatMap()`, which explicitly indicate whether you want to flatten the result or not. The `fib2` function can be rewritten using `flatMap/map` as follows:
1211
+
1. Operations must happen in a specific order.
1212
+
2. Later operations depend on earlier ones.
1213
+
3. You need to limit resource usage by preventing concurrent operations.
// Use sequential execution when operations must happen in order
1217
+
const processData =Effected.allSeq([
1218
+
setupDatabase(),
1219
+
migrateSchema(),
1220
+
importData(),
1221
+
validateData(),
1222
+
]);
1223
+
1224
+
// The equivalent generator syntax
1225
+
const processData =effected(function* () {
1226
+
yield*setupDatabase();
1227
+
yield*migrateSchema();
1228
+
yield*importData();
1229
+
yield*validateData();
1230
+
});
1231
+
```
1232
+
1233
+
For most other cases, it is recommended to use `Effected.all` over `Effected.allSeq`, since it is more efficient and easier to read.
1234
+
1235
+
### Effects without generators (Pipeline syntax)
1236
+
1237
+
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.
1238
+
1239
+
Although using the `effected` helper function with generators allows you to write more imperative-style code using `yield*` to manage effects, this is not the only approach. tinyeffect offers an alternative pipeline-style API for transforming and combining effected programs. At the heart of this API is the `.andThen()` method, which serves as the primary way to transform and chain effected programs.
1240
+
1241
+
While we've covered `.andThen()` in previous sections, we haven’t yet explored how it can be used in a more functional, pipeline-style manner. The `.andThen(handler)` method is quite versatile and can be used in several ways:
1242
+
1243
+
While we have covered `.andThen()` in previous sections, we haven’t yet explored how it can be used in a more functional, pipeline-style manner. Actually, `.andThen(handler)` is quite versatile and can be used in a variety of ways:
1244
+
1245
+
- Transform a result using a pure function.
1246
+
- Chain another effected program.
1247
+
- Work with generators that yield effects.
1248
+
1249
+
Let’s rewrite the `createUser` example using the pipeline syntax:
1250
+
1251
+
```typescript
1252
+
const createUser = (user:Omit<User, "id">) =>
1253
+
requiresAdmin()
1254
+
.andThen(() =>
1255
+
executeSQL("INSERT INTO users (name) VALUES (?)", user.name)
While `andThen` is more concise and easier to read, `flatMap` and `map` provide more control over the result. However, it is not recommended to use `flatMap` and `map` instead of `andThen` directly in most cases, as they can make the code harder to read and understand, and provide very little improvement in performance.
1261
+
A helpful way to understand this code is to think of `Effected` as a container for a delayed computation (or _monad_, if you come from a functional programming background). The `Effected` instance itself doesn’t perform any computation; it only represents a sequence of effects that will be executed when you call `.runSync()` or `.runAsync()`.
1262
+
1263
+
You can compare `Effected` with `Promise` in JavaScript. Just like `Promise.prototype.then(handler)` allows you to chain multiple promises together, `Effected.prototype.andThen(handler)` allows you to chain multiple effected programs together. If a handler returns a generator or another effected program, it will be automatically flattened, similar to how `Promise.prototype.then()` works in JavaScript.
1181
1264
1182
-
There’s also an `.as(value)` method which is an alias for `.map(() => value)`. This can be useful when you want to return a constant value from an effect:
1265
+
To create effects without generators, tinyeffect provides two foundational methods. `Effected.of(value)` creates an effected program that immediately resolves to the given value without performing any effects — similar to `Promise.resolve(value)`. `Effected.from(() => value)` allows you to execute a function lazily when the program is run. These are useful as starting points for pipeline-style code:
1266
+
1267
+
```typescript
1268
+
// Create an effected program that resolves to "Hello, world!"
1269
+
const program1 =Effected.of("Hello, world!")
1270
+
.tap((message) =>println(message))
1271
+
.andThen((message) =>message.toUpperCase());
1272
+
1273
+
// Create an effected program that executes the function when run
When you need more explicit control, tinyeffect offers `.map()` and `.flatMap()`. The `.map()` method transforms a result without introducing new effects, while `.flatMap()` expects the handler to return another effected program:
1281
+
1282
+
```typescript
1283
+
const createUser = (user:Omit<User, "id">) =>
1284
+
requiresAdmin()
1285
+
.flatMap(() =>
1286
+
executeSQL("INSERT INTO users (name) VALUES (?)", user.name)
We also provide an `Effected.all()` helper function to run multiple effected programs in parallel and return their results as an array. This is similar to `Promise.all()`, but it works with effects instead of promises. Here’s an example:
1300
+
In most cases, `.andThen()` is recommended over `.map()` and `.flatMap()` for its versatility and readability. A myth is that `.flatMap()/.map()` may provide better performance than `.andThen()` since they do not need to check if the handler returns a generator or another effected program. However, in practice, the performance difference is negligible, so it’s better to use `.andThen()` directly for simplicity and consistency.
1301
+
1302
+
### Pipeline Syntax V.S. Generator Syntax
1303
+
1304
+
Both pipeline syntax and generator syntax are valid approaches for working with effected programs in tinyeffect. Each approach has distinct advantages:
1305
+
1306
+
**Generator Syntax:**
1307
+
1308
+
- More familiar to developers used to imperative programming.
1309
+
- Natural handling of conditionals and loops.
1310
+
- Simpler debugging with sequential steps.
1311
+
1312
+
**Pipeline Syntax:**
1313
+
1314
+
- More functional approach with method chaining.
1315
+
- Reduces nesting for simple transformations.
1316
+
- Offers better performance in some cases.
1317
+
1318
+
While pipeline syntax offers better performance for simple transformations, in reality such advantages are often negligible since IO-bound effects (like HTTP requests or file operations) usually dominate the execution time. Therefore, the choice between the two should primarily be based on readability and maintainability.
1319
+
1320
+
Below are several examples where pipeline syntax might seem more straightforward:
While the pipeline syntax shown above is more compact, it may not be as readable as the generator syntax for most developers since it involves more nesting.
1386
+
1387
+
Both generator syntax and pipeline syntax are fully supported in tinyeffect — choose whichever approach makes your code most readable and maintainable for you and your team. The best choice often depends on the specific task and your team’s preferences.
0 commit comments