jerelo 1.0.1
jerelo: ^1.0.1 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 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
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(userId)
.thenDo((id) => fetchUserFromApi(id))
.thenIf((user) => user.isActive)
.elseDo((_) => loadUserFromCache(userId))
.thenTap((user) => logAccess(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, A>
A continuation monad representing a computation that will eventually produce a value of type A or terminate with errors.
Type Parameters:
E: The environment type providing context for the continuation executionA: 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 - Core stays stable, extras stay optional
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
- Core Operations - 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 - ContError, ContRuntime, ContObserver
- Construction - Creating continuations with constructors and decorators
- Execution - Running continuations: ContCancelToken, run, ff
- Then Channel - Success path operations: mapping, chaining, tapping, zipping, forking, loops, conditionals
- Else Channel - Error path operations: recovery, fallback, error handling
- Combining - Parallel execution: ContBothPolicy, ContEitherPolicy, both, all, either, any
- Environment - Environment management: local, scope, ask, injection
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, User> fetchUserFromApi(String userId) {
return Cont.ask<AppConfig>().thenDo((config) {
return Cont.fromRun((runtime, observer) {
// Simulate async API call
Future.delayed(config.timeout, () {
if (runtime.isCancelled()) return;
// Simulate successful response
observer.onThen(User(userId, 'John Doe', true));
});
});
});
}
// Simulated cache fallback
Cont<AppConfig, User> loadUserFromCache(String userId) {
return Cont.of(User(userId, 'Cached User', true));
}
// Simulated logging side effect
Cont<AppConfig, void> logAccess(User user) {
return Cont.fromRun((runtime, observer) {
print('[LOG] User accessed: ${user.name}');
observer.onThen(null);
});
}
// Simulated error logging
Cont<AppConfig, void> logError(List<ContError> errors) {
return Cont.fromRun((runtime, observer) {
print('[ERROR] Failed with ${errors.length} error(s)');
for (final e in errors) {
print(' - ${e.error}');
}
observer.onThen(null);
});
}
// Build a computation pipeline
Cont<AppConfig, User> getUserData(String userId) {
return fetchUserFromApi(userId)
// Filter: only proceed if user is active, with custom error
.thenIf(
(user) => user.isActive,
[ContError.capture('User account is not active')],
)
// Fallback: if API fails or user inactive, try cache
.elseDoWithEnv((config, errors) {
return config.enableCache
? loadUserFromCache(userId)
: Cont.stop(errors);
})
// Side effect: log access without blocking
.thenTap((user) => logAccess(user))
// Error logging
.elseTap((errors) => logError(errors));
}
// Fetch multiple users in parallel
Cont<AppConfig, List<User>> getMultipleUsers(List<String> userIds) {
final computations = userIds.map(getUserData).toList();
return Cont.all(
computations,
policy: ContBothPolicy.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 — run returns a ContCancelToken
final token1 = singleUserFlow.run(
prodConfig,
onElse: (errors) => print('Production failed: ${errors.length} error(s)'),
onThen: (user) => print('Production success: ${user.name}'),
);
// Execute the same computation with test config
final token2 = singleUserFlow.run(
testConfig,
onElse: (errors) => print('Test failed: ${errors.length} error(s)'),
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: (errors) => print('Failed to fetch users'),
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 completes first
final racingFlow = Cont.either(
fetchUserFromApi('user456'),
loadUserFromCache('user456'),
policy: ContEitherPolicy.quitFast(), // Return first success
);
racingFlow.run(
prodConfig,
onElse: (errors) => print('Both sources failed'),
onThen: (user) => print('Got user from fastest source: ${user.name}'),
);
// Cancel any running computation when needed
// token1.cancel();
// token2.cancel();
}
Key Features Demonstrated #
The example above showcases:
- Environment Management: Configuration threaded through computations via
AppConfig - Chaining: Sequential operations with
thenDo,thenDoWithEnv - Error Handling: Fallbacks with
elseDo, error logging withelseTap - Conditional Logic: Filtering with
thenIf - Side Effects: Non-blocking logging with
thenTap - Parallel Execution: Multiple computations with
Cont.allandContBothPolicy - Racing: Competing computations with
Cont.eitherandContEitherPolicy - Reusability: Same computation executed with different configurations
- Cancellation: Cooperative cancellation via
ContCancelTokenreturned byrun
License #
See LICENSE file for details.