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
Copy file name to clipboardExpand all lines: README.md
+244Lines changed: 244 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1511,6 +1511,246 @@ While the pipeline syntax shown above is more compact, it may not be as readable
1511
1511
1512
1512
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.
1513
1513
1514
+
### Example: Build a configurable logging system with effects
1515
+
1516
+
Let’s walk through a practical example of using algebraic effects to build a flexible logging system similar to what you might use in a real application. We aim to achieve the following goals:
1517
+
1518
+
- Support multiple logging levels (debug, info, warn, error).
1519
+
- Allow setting minimum logging levels for different parts of the application.
1520
+
- Enable logging to different outputs (console, file, etc.).
1521
+
- Provide a way to customize the logging format.
1522
+
1523
+
In the following example, we’ll achieve this by defining a set of effects for logging, creating default effect handlers that log to the console, and defining several helper functions to manage logging levels and outputs. All of this is implemented in ~60 lines of code.
1524
+
1525
+
**Step 1: Define a dependency for logger**
1526
+
1527
+
We’ll start by defining a dependency for the logger, which will be used to redirect log messages to different outputs.
Note that we allow loggers to be asynchronous, so you can implement a logger that writes to a file or sends logs to a remote server. This is one advantage of algebraic effects: you don’t need to distinguish between synchronous and asynchronous effects, as they are all treated uniformly.
1544
+
1545
+
**Step 2: Create effects for each log level**
1546
+
1547
+
Next, we’ll define effects for each log level. These effects will be used to log messages at different levels.
We define a default effect handler that relies on the `Logger` dependency for each log level. This allows us to control which log levels are enabled at runtime.
1579
+
1580
+
**Step 3: Define handlers for common logging features**
1581
+
1582
+
We’ll define several helper functions to manage logging levels and outputs. The `defineHandlerFor` helper will be used to create these helper functions.
1583
+
1584
+
The first helper is `withPrefix(prefixFactory: (level) => string)`, which adds a prefix to each log message based on the log level. This is useful for distinguishing between different log levels in the output.
yieldeffect; // Re-yield the effect with the modified payloads
1596
+
resume();
1597
+
},
1598
+
),
1599
+
);
1600
+
}
1601
+
```
1602
+
1603
+
The next is `withMinimumLogLevel(level)`, which filters out log messages below a specified minimum level. This allows you to control the verbosity of the logs based on the current logging level.
// Change default handler of disabled log levels to resume immediately
1618
+
effect.defaultHandler= ({ resume }) =>resume();
1619
+
yieldeffect; // Re-yield the effect with the modified default handler
1620
+
resume();
1621
+
},
1622
+
);
1623
+
});
1624
+
}
1625
+
```
1626
+
1627
+
**Step 4: Use the logging system!**
1628
+
1629
+
Done! We’ve already created a fully functional logging system. But now you might wonder how to use it in practice. Let’s start by creating a simple program that uses the logging system:
1630
+
1631
+
```typescript
1632
+
const program =effected(function* () {
1633
+
yield*logDebug("Debug message");
1634
+
yield*logInfo("Info message");
1635
+
yield*logWarn("Warning!");
1636
+
yield*logError("Error occurred!");
1637
+
});
1638
+
1639
+
awaitprogram.runAsync();
1640
+
```
1641
+
1642
+
We do not explicitly handle any logging effects, so the default handler will be used, which logs to the console. The output will look like this:
1643
+
1644
+
```text
1645
+
Debug message
1646
+
Info message
1647
+
Warning!
1648
+
Error occurred!
1649
+
```
1650
+
1651
+
By default, all log levels are enabled, so all messages are printed. Now, let’s set the minimum log level to `warn` to disable debug and info messages:
1652
+
1653
+
```typescript
1654
+
const program =effected(function* () {
1655
+
// ...
1656
+
}).pipe(withMinimumLogLevel("warn"));
1657
+
```
1658
+
1659
+
The output will now only show the warning and error messages:
1660
+
1661
+
```text
1662
+
Warning!
1663
+
Error occurred!
1664
+
```
1665
+
1666
+
For now, it’s not easy to distinguish between different log levels. To add a prefix to each log message, we can use the `withPrefix` helper function:
1667
+
1668
+
```typescript
1669
+
function logPrefix(level:LogLevel) {
1670
+
const date =newDate();
1671
+
const yyyy =date.getFullYear();
1672
+
const MM =String(date.getMonth() +1).padStart(2, "0");
What if we want to log to a file instead of the console? We can create a custom logger that writes to a file and provide it as the `Logger` dependency. Here’s an example of how to do this:
In this example, we use [showify](https://github.com/Snowflyt/showify) to format the log messages into human-readable strings. The `fileLogger` function accepts a file path and returns a logger object that writes log messages to that file. We use `node:fs/promises` to handle file operations asynchronously.
1722
+
1723
+
Now, instead of logging to the console, all log messages will be written to the `log.txt` file. You can customize the logger to write to different outputs, such as a database or a remote server.
1724
+
1725
+
You can also create a helper function to combine multiple handlers together:
1726
+
1727
+
```typescript
1728
+
function dualLogger(logger1:Logger, logger2:Logger) {
Now, all log messages will be logged to both the console and the `log.txt` file.
1751
+
1752
+
To extend the logging system, you can create additional effects for other log levels, such as `trace`, `fatal`, or a `log` effect that defaults to `info` but can be configured to use any log level. In real applications, you can also redirect log messages to a worker thread and then handle them with a message queue, allowing you to log messages without blocking the main thread.
1753
+
1514
1754
## FAQ
1515
1755
1516
1756
### What’s the relationship between tinyeffect and Effect?
@@ -1520,3 +1760,7 @@ It is a coincidence that the name “tinyeffect” is similar to [Effect](https:
1520
1760
However, it is not surprising that the two libraries share some similarities, as they both aim to provide a way to handle side effects in a type-safe manner. The `Effect` type in Effect and the `Effected` type in tinyeffect are both monads that abstract effectful computations, and they share similarities in their API design, e.g., `Effect.map()` vs. `Effected.prototype.map()`, `Effect.flatMap()` vs. `Effected.prototype.flatMap()`, `Effect.andThen()` vs. `Effected.prototype.andThen()`, etc.
1521
1761
1522
1762
While sharing some similarities, tinyeffect and Effect are fundamentally different in their design and implementation. tinyeffect is designed to be a lightweight library that focuses on providing a simple and intuitive API for handling side effects, while Effect is designed to be a full-fledged library that provides a comprehensive set of features for building effectful applications. Also, both libraries provide concurrency primitives, but Effect uses a fiber-based concurrency model, whereas tinyeffect uses a simple iterator-based model.
0 commit comments