jerelo 3.0.0
jerelo: ^3.0.0 copied to clipboard
A minimal, lawful Dart functional toolkit for composing synchronous and asynchronous workflows using continuation-passing style (CPS).
Jerelo #
Jerelo is a Dart library for building cold, lazy, reusable computations using continuation-passing style. It provides a composable Cont type with three outcome channels — success, typed errors, and crashes — plus operations for chaining, transforming, branching, merging, and error handling.
The name "Jerelo" is Ukrainian for "source" or "spring" - each Cont is a source of results that can branch, merge, filter, and transform data through your workflows.
What Makes Jerelo Different? #
Unlike Dart's Future which starts executing immediately upon creation, Cont is:
- Cold: Doesn't run until you explicitly call
run - Pure: No side effects during construction
- Lazy: Evaluation is deferred
- Reusable: Can be safely executed multiple times with different configurations
- Composable: Build complex flows from simple, testable pieces
- Cancellable:
runreturns a token whosecancel()is forwarded to the running computations, which decide how to stop
This makes Cont ideal for defining complex business logic that needs to be tested, reused across different contexts, or configured at runtime.
Quick Example #
import 'package:jerelo/jerelo.dart';
// Define a computation (doesn't execute yet)
final getUserData = Cont.of<AppConfig, String, String>(userId)
.thenDo((id) => fetchUserFromApi(id))
.thenDo<User>((user) => user.isActive ? Cont.of(user) : Cont.error('User is inactive'))
.elseDo((_) => loadUserFromCache(userId))
.thenMap((user) {
logAccess(user);
return user;
});
// Execute it multiple times with different configs
getUserData.run(prodConfig, onElse: handleError, onThen: handleSuccess);
getUserData.run(testConfig, onElse: handleError, onThen: handleSuccess);
What is Cont? #
final class Cont<E, F, A>
A continuation monad representing a computation that will eventually produce a value of type A, terminate with a typed error of type F, or crash with an unexpected exception.
Type Parameters:
E: The environment type providing context for the continuation executionF: The error type for expected, typed failures on the else channelA: The value type that the continuation produces upon success
Design Goals #
- Pure Dart - No platform dependencies
- Zero dependencies - Minimal footprint
- Minimal API - Learn once, use everywhere
- Modular boundaries - Swap HTTP/DB/cache/logging without rewriting flows
- Execution agnostic - Same flow works sync, async, and in tests
- Explicit failures - Unexpected throws become explicit termination
- Extensible - Write your own operators on the public API
Documentation #
User Guide #
A comprehensive guide to understanding and using Jerelo:
- Introduction & Core Concepts - Understanding computations and continuation-passing style
- Fundamentals: Construct & Run - Learn to create and execute computations
- Composing Computations - Master mapping, chaining, branching, and error handling
- Racing and Merging - Parallel execution, racing, and complex workflows
- Environment Management - Configuration and dependency injection patterns
- Extending Jerelo - Create custom operators and computations
- Complete Examples - Real-world patterns and use cases
API Reference #
Complete reference for all public APIs:
- Types - ContCrash, CrashOr, ContCancelToken, ContObserver
- Construction - Creating continuations: fromRun, of, error, crash, askThen, askElse, fromDeferred, bracket, decorate
- Execution - Running continuations: run, runWith, cancellation; extensions: flatten, thenAbsurd, elseAbsurd, thenUnit, elseUnit
- Then Channel - Success path operations: mapping, chaining, tapping, zipping, forking, demotion, conditionals, loops
- Else Channel - Error path operations: fallback, transformation, tapping, zipping, forking, promotion, conditionals, retry loops
- Crash Channel - Crash recovery operations: recovery, side effects, zipping, forking, recovery to then or else, conditionals, retry loops
- Combining - Parallel execution: OkPolicy, CrashPolicy, both, all, either, any, coalesce, converge, and, or, coalesceWith
- Environment - Environment management: local, local0, withEnv, thenInject, injectedByThen, elseInject, injectedByElse
Most operators come with variants that ignore the value (thenMap0), read the environment (thenDoWithEnv, thenDoWithEnv0) or use a constant (thenMapTo). You can write your own operators on the public API in the same way (see Extending Jerelo).
Full Example #
Here's a complete example demonstrating key Jerelo features:
import 'dart:async';
import 'package:jerelo/jerelo.dart';
// Configuration injected as environment
class AppConfig {
final String apiUrl;
final Duration timeout;
final bool enableCache;
AppConfig(this.apiUrl, this.timeout, this.enableCache);
}
// Domain model
class User {
final String id;
final String name;
final bool isActive;
User(this.id, this.name, this.isActive);
}
// Simulated API call
Cont<AppConfig, String, User> fetchUserFromApi(String userId) {
return Cont.askThen<AppConfig, String>().thenDo((config) {
return Cont.fromRun((env, observer) {
// Simulate async API call
final timer = Timer(config.timeout, () {
// Simulate successful response
observer.onThen(User(userId, 'John Doe', true));
});
// Cancelling stops the call and reports it as an error
return () {
timer.cancel();
observer.onElse('Request cancelled');
};
});
});
}
// Simulated cache fallback
Cont<AppConfig, String, User> loadUserFromCache(String userId) {
return Cont.of(User(userId, 'Cached User', true));
}
// Simulated logging side effect
void logAccess(User user) {
print('[LOG] User accessed: ${user.name}');
}
// Build a computation pipeline
Cont<AppConfig, String, User> getUserData(String userId) {
return fetchUserFromApi(userId)
// Filter: only proceed if user is active, with custom error
.thenDo<User>((user) {
return user.isActive
? Cont.of(user)
: Cont.error('User account is not active');
})
// Fallback: if API fails or user inactive, try cache
.elseDo((error) {
return Cont.askThen<AppConfig, String>().thenDo<User>((config) {
return config.enableCache
? loadUserFromCache(userId)
: Cont.error(error);
});
})
// Side effect: log access and pass the value through
.thenMap((user) {
logAccess(user);
return user;
})
// Recover unexpected exceptions into typed errors
.crashDo((crash) => Cont.error('Unexpected error: $crash'));
}
// Fetch multiple users in parallel
Cont<AppConfig, String, List<User>> getMultipleUsers(List<String> userIds) {
final computations = userIds.map(getUserData).toList();
return Cont.all(
computations,
policy: OkPolicy.quitFast(), // Fail fast on first error
);
}
void main() {
// Production configuration
final prodConfig = AppConfig(
'https://api.example.com',
Duration(seconds: 5),
true,
);
// Test configuration
final testConfig = AppConfig(
'http://localhost:3000',
Duration(milliseconds: 100),
false,
);
print('=== Single User Example ===');
// Define the computation once
final singleUserFlow = getUserData('user123');
// Execute with production config
singleUserFlow.run(
prodConfig,
onCrash: (crash) => print('Production crash: $crash'),
onElse: (error) => print('Production failed: $error'),
onThen: (user) => print('Production success: ${user.name}'),
);
// Execute the same computation with test config
singleUserFlow.run(
testConfig,
onElse: (error) => print('Test failed: $error'),
onThen: (user) => print('Test success: ${user.name}'),
);
print('\n=== Multiple Users Example ===');
// Fetch multiple users in parallel
final multiUserFlow = getMultipleUsers(['user1', 'user2', 'user3']);
multiUserFlow.run(
prodConfig,
onElse: (error) => print('Failed to fetch users: $error'),
onThen: (users) => print('Fetched ${users.length} users: ${users.map((u) => u.name).join(', ')}'),
);
print('\n=== Advanced: Racing API vs Cache ===');
// Race API against cache, return whichever succeeds first
// (a failure waits for the other source)
final racingFlow = Cont.either(
fetchUserFromApi('user456'),
loadUserFromCache('user456'),
(e1, e2) => '$e1; $e2', // Combine errors if both fail
policy: OkPolicy.quitFast(), // Return first success
);
racingFlow.run(
prodConfig,
onElse: (error) => print('Both sources failed: $error'),
onThen: (user) => print('Got user from fastest source: ${user.name}'),
);
}
Key Features Demonstrated #
The example above showcases:
- Three-channel design: Success (then), typed errors (else), and crashes for unexpected exceptions
- Environment Management: Configuration threaded through computations via
AppConfig - Chaining: Sequential operations with
thenDo - Error Handling: Fallbacks with
elseDo, conditional caching that reads the environment withCont.askThen - Crash Recovery: Converting unexpected exceptions into typed errors with
crashDo - Conditional Logic: Filtering with
thenDothat returnsCont.ofor a typedCont.error - Side Effects: Logging inside
thenMapwhile passing the original value through - Parallel Execution: Multiple computations with
Cont.allandOkPolicy - Racing: Competing computations with
Cont.eitherand error combining - Cancellation:
fetchUserFromApireturns a canceller that stops its timer, so thequitFastrace cancels the API call once the cache wins - Reusability: Same computation executed with different configurations
License #
See LICENSE file for details.