ribs_effect 1.0.0-dev.5
ribs_effect: ^1.0.0-dev.5 copied to clipboard
Functional effect system with IO monad, safe concurrent state (Ref), and resource management and pure synchronziation primitive (Deferred).
ribs_effect #
ribs_effect is a high-performance, purely functional effect system for Dart, heavily inspired by Cats Effect. It provides a robust foundation for building asynchronous and resource-safe applications.
At the core of the library is IO, a data type that describes an effect as a value. This allows you to treat side-effects (like network calls, file I/O, or console output) as referentially transparent values that can be composed, transformed and passed around before being executed.
Key Features #
- Purely Functional: Side-effects are deferred until the "end of the world," making your code easier to reason about and test.
- Fiber-based Concurrency: Lightweight fibers provide efficient concurrency that's easier to manage than raw Futures.
- Interruption/Cancelation: Built-in support for safely canceling running tasks.
- Resource Safety: Guaranteed cleanup of resources (like file handles or sockets) even in the face of errors or cancelation.
- Rich Standard Library: Includes concurrent primitives like
Ref,Deferred,Semaphore, andQueue.
Documentation #
For more detailed information, guides, and API documentation, visit the Full Ribs Documentation Site.
Use Cases #
Composing Effects #
Easily sequence and transform effects using standard functional combinators.
final program = IO.delay(() => 42)
.map((n) => n * 2)
.flatMap((result) => IO.print('The result is $result'));
await program.unsafeRunFuture();
Safe Resource Management #
Ensure resources are always released, no matter what happens during execution.
final result = IO.print('Opening file...').bracket(
(file) => processFile(file), // use
(file) => IO.print('Closing file...'), // release
);
The Resource Type #
While bracket is great for simple cases, Resource is a first-class data type that wraps acquisition and release logic. It's a tool for managing resources, allowing you to:
- Compose multiple resources easily.
- Ensure resources are released in the correct (reverse) order.
- Avoid "callback hell" of nested
bracketcalls.
final server = Resource.make(
IO.print('Starting server...').as('localhost:8080'),
(addr) => IO.print('Stopping server at $addr...'),
);
final connection = Resource.make(
IO.print('Opening connection...').as('conn_123'),
(id) => IO.print('Closing connection $id...'),
);
// Compose resources to get both values
final app = server.product(connection);
// Both are acquired, provided to the block, then released in reverse order.
await app.use((data) {
final (server, connection) = data;
return IO.print('App at $server and connection $connection');
}).unsafeRunFuture();
Coordination with Ref and Deferred #
Synchronize and share state between concurrent fibers.
final coordination = IO.deferred<int>().flatMap((deferred) {
return ilist([
IO.sleep(const Duration(seconds: 1)).flatMap((_) => deferred.complete(42)),
deferred.get().flatMap((value) => IO.print('Received: $value')),
]).parSequence();
});
Concurrent Execution #
Run multiple independent tasks concurrently and gather their results safely.
final tasks = ilist([
fetchData(1),
fetchData(2),
fetchData(3),
]);
// Runs all tasks concurrently and returns an IList of results
final results = await tasks.parSequence().unsafeRunFuture();
Fiber Cancelation #
Fibers can be canceled to stop ongoing work. This is handled safely, ensuring that any attached finalizers are executed.
final program = IO.print('Working...').delayBy(Duration(seconds: 10))
.onCancel(IO.print('Cleanup done!'))
.start()
.flatMap((fiber) => fiber.cancel());
await program.unsafeRunFuture();
Fine Grained Cancelation #
Use uncancelable to prevent an effect from being interrupted. You can use the provided Poll to create specific regions where cancelation is allowed again.
final safeOperation = IO.uncancelable((poll) =>
IO.print('Acquired').flatMap((_) =>
poll(IO.print('Interruptible work...'))
.guarantee(IO.print('Released'))
)
);
Testing with Ticker #
ribs_effect provides a Ticker runtime for deterministic testing of time-based effects without actually waiting for real time to pass.
import 'package:ribs_effect/test.dart';
test('delayed effect', () async {
final io = IO.sleep(Duration(seconds: 1)).as('Done');
final ticker = io.ticked;
ticker.advance(Duration(milliseconds: 500));
expect(await ticker.outcome.isCompleted, isFalse);
ticker.advanceAndTick(Duration(milliseconds: 500));
expect(await ticker.outcome, ioSucceeded('Done'));
});
IO Tracing #
Tracing provides a high-level view of your IO execution history, which is much easier to debug than raw Dart stack traces.
Enabling Tracing
Tracing must be enabled globally before running any effects:
IOTracingConfig.tracingEnabled = true;
Manual Tracing #
You can add labels to specific points in your program to make traces more informative:
final tracked = IO.pure(42).traced('my-trace-point');
When an error occurs, the resulting StackTrace will be an IOFiberTrace containing the history of executed effect labels.
Example #
Check out the full example program in example/example.dart.