effect_dart 0.1.0
effect_dart: ^0.1.0 copied to clipboard
A powerful Effect library for Dart inspired by Effect-TS, providing functional programming patterns for managing side effects, errors, and dependencies.
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.