daxle
A lightweight, type-safe functional programming toolkit for Dart. Designed to replace unsafe patterns (like throwing exceptions or abusing null) with explicit, declarative types and pipelines.
Inspired by functional features in Rust, Haskell, and Scala.
What's New in v2.0 🚀
Version 2.0 represents a complete API redesign to leverage modern Dart features (such as sealed classes, pattern matching, and records).
- Removed types:
Lazy<T>has been removed to stream-line composition. - Sealed Option, Either & Result:
Option,Either, andResultare now sealed classes. You can utilize compile-time exhaustive switch-matching. - Added TaskEither: Introduces lazy, asynchronous computations that can fail (
Future<Either<L, R>>), ensuring safe and composable async logic. - Added Pipelines:
Pipeline(synchronous) andAsyncPipeline(asynchronous) provide type-safe deferred operation chaining, logging/observability taps, error recovery, and concurrent execution. - Added Unit: Represents the absence of a value, allowing type-safe generic returns.
Installation
Add the dependency to your pubspec.yaml:
dependencies:
daxle: ^2.2.0
Then, run:
dart pub get
Core Types
1. Option<T>
An alternative to nullable values (T?). Represents either the presence of a value (Some) or the absence of a value (None).
import 'package:daxle/daxle.dart';
void main() {
final Option<int> someValue = .some(42);
final Option<int> noValue = .none();
final Option<int> fromNull = .fromNullable(null); // Resolves to None
final Option<int> fromPred = .fromPredicate(10, (v) => v > 5); // Some(10)
// Filter option value:
final filtered = someValue.filter((v) => v > 100); // None
// Transform with map or flatMap
final mapped = someValue.map((v) => 'The answer is $v');
print(mapped); // Some(The answer is 42)
// Retrieve values safely
final int val = noValue.getOrElse(0); // Returns 0
// Exhaustive pattern matching (enforced at compile-time!)
final message = switch (someValue) {
Some(value: final v) => 'Found: $v',
None() => 'Nothing here',
};
}
2. Either<L, R>
Represents a value of one of two possible types. By convention, Right is success/expected and Left is error/failure.
import 'package:daxle/daxle.dart';
Either<String, int> divide(int a, int b) {
if (b == 0) return const .left('Cannot divide by zero');
return .right(a ~/ b);
}
void main() {
final result = divide(10, 2);
// Construct based on boolean condition:
final resultCond = .cond(true, 5, 'Cannot divide by zero');
// Check state
if (result.isRight) {
print('Successful division!');
}
// Fold to extract values safely
final message = result.fold(
(leftError) => 'Failure: $leftError',
(rightVal) => 'Result: $rightVal',
);
}
3. Result<T, E>
Represents the result of an operation that can succeed (Ok) or fail (Err). An alternative to throwing exceptions or returning nullable types.
import 'package:daxle/daxle.dart';
Result<int, String> parsePort(String value) {
final port = int.tryParse(value);
if (port == null) return const .err('Invalid port number');
return .ok(port);
}
void main() {
final result = parsePort('8080');
// fold to handle both success and error cases
result.fold(
onOk: (port) => print('Port is $port'), // Port is 8080
onErr: (error, stack) => print('Failed to parse: $error'),
);
// transform success or error values
final mapped = result.map((p) => p + 1); // Ok(8081)
}
4. TaskEither<L, R>
Represents a lazy, asynchronous computation that returns an Either<L, R>. It provides significant advantages over a raw Future<Either<L, R>> and AsyncPipeline:
- Lazy Execution: Both
TaskEitherand AsyncPipeline are lazy blueprints. They do not run until.run()is called, allowing easy retries or fallbacks via.orElse. - Monadic Error Short-Circuiting: Unlike AsyncPipeline (which relies on standard exceptions propagating until caught by
recover),TaskEitherembeds theEitherstate at each step. If a step resolves to aLeft, subsequent steps are short-circuited. - Linear Chaining: Allows chaining dependent async operations via
flatMapwithout nestingawaitandfoldblocks. - Exception Guarding: Automatically catches runtime exceptions and maps them to a safe
Left(L).
import 'package:daxle/daxle.dart';
TaskEither<String, String> fetchUser(int id) => .fromFuture(
() async => 'User #$id',
(err, _) => 'User not found',
);
TaskEither<String, String> fetchConfig(String role) => .fromFuture(
() async => 'Config for $role',
(err, _) => 'Config not found',
);
void main() async {
// Chain dependent async computations flatly:
final task = fetchUser(42)
.flatMap((user) => fetchConfig(user));
final Either<String, String> result = await task.run();
}
5. Pipeline<T> & AsyncPipeline<T>
Deferred, type-safe pipelines to chain operations sequentially. Supports logging (tap), error translation (mapError), fallback recovery (recover/recoverWith), cleanup (finalize), and concurrent combinations (zip).
import 'package:daxle/daxle.dart';
void main() async {
// 1. Synchronous Pipeline
final syncVal = Pipeline(() => 10)
.pipe((x) => x * 2)
.tap((x) => print('Stage: $x')) // Observational side-effect
.recover((err, stack) => -1) // Recovers if any error occurred
.run(); // Pipeline evaluates lazily here
// 2. Asynchronous Pipeline
final asyncVal = await AsyncPipeline(() => Future.value(100))
.pipe((x) async => x + 50)
.run();
// 3. Concurrent pipelines execution
final p1 = AsyncPipeline(() => Future.value(10));
final p2 = AsyncPipeline(() => Future.value(20));
final zipped = p1.zip(p2, (a, b) => a + b);
final sum = await zipped.run(); // Concurrently waits and combines: 30
}
6. Unit
A singleton type containing exactly one value: unit. Used in functional programming to represent the absence of a meaningful value in generic constructs (like returning Either<String, Unit>).
import 'package:daxle/daxle.dart';
Either<String, Unit> saveRecord(String data) {
try {
// Save logic...
return const .right(unit);
} catch (e) {
return .left('Failed to save: $e');
}
}
7. Async Utilities
Provides re-exported utilities from package:async to simplify asynchronous control flow and stream handling:
- Future Utilities:
FutureGroup: A collection of futures that waits for all added futures to complete.AsyncCache: Caches the results of asynchronous operations.AsyncMemoizer: Memorizes the result of an asynchronous closure to run it only once.
- Stream Utilities:
StreamZip: Combines multiple streams into a single stream of zipped lists.StreamQueue: Simplifies pull-based stream consumption.StreamGroup: Merges multiple streams into a single output stream.StreamSplitter: Splits a stream into multiple identical, independent copies.
import 'package:daxle/daxle.dart';
void main() async {
final streamA = Stream.fromIterable([1, 2]);
final streamB = Stream.fromIterable(['A', 'B']);
// StreamZip is re-exported from package:async
final zipped = StreamZip([streamA, streamB]);
await for (final pair in zipped) {
print(pair); // [1, 'A'], then [2, 'B']
}
}
Contributing
Contributions are welcome! This package is part of the daxle monorepo workspace. Please see the root repository for workspace contribution guidelines.
License
daxle is released under the MIT License.
Libraries
- daxle
daxleis a library that provides a set of functional programming constructs inspired by languages like Rust and Haskell. It is designed to enhance the robustness and clarity of Dart applications by offering explicit, type-safe mechanisms for handling fallible operations, optional values, and deferred computation pipelines.