Effect.dart
A powerful Effect library for Dart inspired by Effect-TS, providing functional programming patterns for managing side effects, errors, and dependencies in a type-safe and composable way.
Features
- Type-safe Effects: Encode success, error, and dependency types in the type system
- Lazy Evaluation: Effects are descriptions that don't execute until run
- Error Handling: Built-in error handling with typed errors and causes
- Dependency Injection: Type-safe context system for managing dependencies
- Concurrency: Built-in support for concurrent and parallel execution
- Composability: Chain and combine effects using functional operations
The Effect Type
Effect<Success, Error, Requirements>
Where:
Success
: The type of value the effect produces on successError
: The type of expected errors that can occurRequirements
: The type of dependencies required from context
Quick Start
Add to your pubspec.yaml
:
dependencies:
effect.dart: ^0.1.0
Basic Usage
Creating Effects
import 'package:effect/effect.dart';
// Success effect
final success = Effect.succeed(42);
// Failure effect
final failure = Effect.fail('Something went wrong');
// Sync computation that might throw
final risky = Effect.sync(() {
if (Random().nextBool()) {
return 'Success!';
} else {
throw Exception('Failed!');
}
});
// Async computation
final async = Effect.async(() async {
await Future.delayed(Duration(seconds: 1));
return 'Done!';
});
Running Effects
// Get the exit result (Success or Failure)
final exit = await effect.runToExit();
// Run unsafely (throws on failure)
final result = await effect.runUnsafe();
// Using the runtime directly
final runtime = Runtime.defaultRuntime;
final exit2 = await runtime.runToExit(effect);
Transforming Effects
// Map success values
final mapped = Effect.succeed(21)
.map((x) => x * 2);
// Chain effects
final chained = Effect.succeed(10)
.flatMap((x) => Effect.succeed(x + 5))
.flatMap((x) => Effect.succeed(x.toString()));
// Handle errors
final recovered = Effect.fail('error')
.catchAll((err) => Effect.succeed('fallback'));
// Map errors
final mappedError = Effect.fail(404)
.mapError((code) => 'HTTP Error: $code');
Dependency Injection
// Define services
class DatabaseService {
Future<String> getData(String id) async => 'Data for $id';
}
class LoggerService {
void log(String message) => print('[LOG] $message');
}
// Create effects that require services
final dbEffect = Effect.service<DatabaseService>()
.flatMap((db) => Effect.async(() => db.getData('user123')));
final logEffect = Effect.service<LoggerService>()
.flatMap((logger) => Effect.sync(() => logger.log('Done')));
// Provide services via context
final context = Context.empty()
.add(DatabaseService())
.add(LoggerService());
// Or provide individual services
final effectWithService = dbEffect.provideService(DatabaseService());
// Run with context
final result = await dbEffect.runToExit(context);
Concurrent Execution
final effect1 = Effect.async(() => Future.delayed(Duration(milliseconds: 100), () => 'A'));
final effect2 = Effect.async(() => Future.delayed(Duration(milliseconds: 200), () => 'B'));
final effect3 = Effect.async(() => Future.delayed(Duration(milliseconds: 150), () => 'C'));
// Run all concurrently
final results = await Runtime.defaultRuntime.runConcurrently([
effect1, effect2, effect3
]);
// Race (first to complete wins)
final winner = await Runtime.defaultRuntime.runRace([
effect1, effect2, effect3
]);
// Using fibers for fine-grained control
final fiber1 = effect1.fork();
final fiber2 = effect2.fork();
final (exit1, exit2) = await fiber1.zip(fiber2);
Advanced Examples
Error Recovery Pipeline
final pipeline = Effect.succeed('https://api.example.com/data')
.flatMap((url) => httpGet(url))
.mapError((httpError) => 'Network error: $httpError')
.catchAll((error) => Effect.succeed('{"fallback": true}'))
.flatMap((json) => parseJson(json))
.map((data) => transformData(data));
final result = await pipeline.runToExit();
Service-Oriented Architecture
abstract class UserRepository {
Future<User?> findById(String id);
}
abstract class EmailService {
Future<void> sendEmail(String to, String subject, String body);
}
final sendWelcomeEmail = (String userId) =>
Effect.service<UserRepository>()
.flatMap((repo) => Effect.async(() => repo.findById(userId)))
.flatMap((user) => user != null
? Effect.service<EmailService>()
.flatMap((email) => Effect.async(() =>
email.sendEmail(user.email, 'Welcome!', 'Welcome to our service!')))
: Effect.fail('User not found'))
.map((_) => 'Email sent successfully');
// Provide all dependencies
final context = Context.empty()
.add<UserRepository>(DatabaseUserRepository())
.add<EmailService>(SmtpEmailService());
final result = await sendWelcomeEmail('user123').runToExit(context);
API Reference
Effect
Effect.succeed<A>(A value)
- Create successful effectEffect.fail<E>(E error)
- Create failed effectEffect.sync<A>(A Function())
- Sync computationEffect.async<A>(Future<A> Function())
- Async computationEffect.service<A>()
- Require service from context
Instance Methods
map<B>(B Function(A))
- Transform success valuemapError<E2>(E2 Function(E))
- Transform error valueflatMap<B>(Effect<B, E2, R2> Function(A))
- Chain effectscatchAll<E2>(Effect<A, E2, R2> Function(E))
- Handle errorsprovideContext(Context<R>)
- Provide contextprovideService<S>(S)
- Provide single servicerunToExit([Context<R>?])
- Execute and get ExitrunUnsafe([Context<R>?])
- Execute unsafely
Context
Context.empty()
- Empty contextContext.of<T>(T service)
- Single service contextadd<T>(T service)
- Add serviceget<T>()
- Retrieve servicehas<T>()
- Check if service existsmerge(Context other)
- Combine contexts
Runtime
Runtime.defaultRuntime
- Default runtime instancerunToExit<A, E, R>(Effect<A, E, R>)
- Execute effectrunUnsafe<A, E, R>(Effect<A, E, R>)
- Execute unsafelyrunConcurrently<A, E, R>(List<Effect<A, E, R>>)
- Concurrent executionrunRace<A, E, R>(List<Effect<A, E, R>>)
- Race executionfork<A, E, R>(Effect<A, E, R>)
- Create fiber
Exit
Exit.succeed<A>(A value)
- Success exitExit.fail<E>(E error)
- Failure exitExit.die(Object throwable)
- Defect exitmap<B>(B Function(A))
- Transform successfold<C>(C Function(Cause<E>), C Function(A))
- Fold to value
Either
Either.left<L, R>(L value)
- Left valueEither.right<L, R>(R value)
- Right valuemap<R2>(R2 Function(R))
- Transform rightflatMap<R2>(Either<L, R2> Function(R))
- Chain eithersfold<T>(T Function(L), T Function(R))
- Fold to value
Running Examples
# Run the basic example
dart run example/basic_example.dart
# Run tests
dart test
# Run with melos (if you have it installed)
melos test
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Inspiration
This library is heavily inspired by Effect-TS, bringing similar concepts and patterns to the Dart ecosystem. Special thanks to the Effect-TS team for their innovative work in functional programming.
Libraries
- effect_dart
- A powerful Effect library for Dart inspired by Effect-TS.