daxle

Pub Version Pub Points License: MIT

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, and Result are 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) and AsyncPipeline (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 TaskEither and 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), TaskEither embeds the Either state at each step. If a step resolves to a Left, subsequent steps are short-circuited.
  • Linear Chaining: Allows chaining dependent async operations via flatMap without nesting await and fold blocks.
  • 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
daxle is 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.