It's while exploring the new features of Dart 3 and its sealed classes that I felt like packaging this little piece of code to exploit the power of pattern matching.
Because no matter what people say, functional programming can be really cool and powerful 😏
This package is a simple implementation of the Result Monad, which is a way to handle errors in a functional way. It's a simple wrapper around a value that can be either a success or a failure. It's a way to avoid throwing exceptions and to handle errors in a more explicit way.
The used naming convention is inspired by the Rust Result enum. (So you will find Ok and Err ).
Note those code are pseudo-code, check tests and examples for more details and real code.
Different ways to create a Result:
Result.ok(value)to create a successful resultOk(value)same asResult.ok(value)Result.success(value)same asResult.ok(value)Result.err(error, [stackTrace])to create a failed resultErr(error, [stackTrace])same asResult.err(error, [stackTrace])Result.error(error, [stackTrace])same asResult.err(error, [stackTrace])Result.failure(error, [stackTrace])same asResult.err(error, [stackTrace])Result.from(() => value)to create a result from a function that can throwResult.fromAsync(() => future)to create a result from a future that can failResult.fromCondition({condition, value, error})to create a result from a conditionResult.fromConditionLazy({condition: () => condition, value: () => value, error: () => error})to create a result from a condition with lazy evaluation
Different ways to extract the value from a Result:
result.okto get the value if the result is successful and discard the error if it's a failureresult.errto get the error if the result is a failure and discard the value if it's a successresult.stackTraceto get the stack trace if the result is a failureresult.expect()to get the value if the result is successful or throw an exception if it's a failureresult.expectErr()to get the error if the result is a failure or throw an exception if it's a successresult.unwrap()same asresult.expect()result.unwrapErr()same asresult.expectErr()result.unwrapOr(defaultValue)to get the value contained in the result or a default value if it's a failureresult.unwrapOrElse((error) => defaultValue)to get the value contained in the result or a default value if it's a failure with lazy evaluation
Different ways to inspect the value of a Result:
result.isOkto check if the result is successfulresult.isErrto check if the result is a failureresult.contains(value)to check if the result contains a specific valueresult.containsErr(error)to check if the result contains a specific errorresult.containsLazy(() => value)to check if the result contains a specific value with lazy evaluationresult.containsErrLazy(() => error)to check if the result contains a specific error with lazy evaluationresult.inspect((value) => void)to inspect the value of the resultresult.inspectErr((error) => void)to inspect the error of the resultresult1 == result2to check if two results are equalresult1 != result2to check if two results are not equal
Different ways to transform a Result:
result.map<U>(U Function(value) transform)to transform the value of the resultresult.mapAsync<U>(FutureOr<U> Function(value) transform)to transform the value of the result asynchronouslyresult.mapErr<U>(U Function(error) transform)to transform the error of the resultresult.mapErrAsync<U>(FutureOr<U> Function(error) transform)to transform the error of the result asynchronouslyresult.mapOr<U>(U defaultValue, U Function(value) transform)to transform the value of the result or return a default value if it's a failureresult.mapOrAsync<U>(FutureOr<U> defaultValue, FutureOr<U> Function(value) transform)to transform the value of the result or return a default value if it's a failure asynchronouslyresult.mapOrElse<U>(U Function(value) defaultFn, U Function(error) transform)to transform the value and the error of the resultresult.mapOrElseAsync<U>(FutureOr<U> Function(value) defaultFn, FutureOr<U> Function(error) transform)to transform the value and the error of the result asynchronouslyresult.fold<U>(U Function(value) okFn, U Function(error) errFn)same asmapOrElsewith a different syntaxresult.foldAsync<U>(FutureOr<U> Function(value) okFn, FutureOr<U> Function(error) errFn)same asmapOrElseAsyncwith a different syntaxresult.flatten()to flatten a result of typeResult<Result<T,E>,E>into a result of typeResult<T,E>result.and<U>(Result<U,E> other)to combine two resultsresult.andThen<U>(Result<U,E> Function(value) transform)to combine two results with a functionresult.or<F>(Result<T,F> other)to combine two resultsresult.orElse<F>(Result<T,F> Function(error) transform)to combine two results with a functionresult1 & result2to combine two results with theandoperatorresult1 | result2to combine two results with theoroperator
Import the package:
import 'package:sealed_result/sealed_result.dart';Create a function that can fail:
enum Version { version1, version2 }
Result<Version, ResultException> parseVersion(List<int> header) =>
switch (header) {
final header when header.isEmpty =>
const Result.err(ResultException('invalid header length')),
final header when header[0] == 1 => const Result.ok(Version.version1),
final header when header[0] == 2 => const Result.ok(Version.version2),
_ => const Result.err(ResultException('invalid version')),
};Use the function:
final version = parseVersion([1, 2, 3, 4]);
print(
switch (version) {
Ok(ok: final value) => 'working with version: $value',
Err(err: final error) => 'error parsing header: $error',
},
);If, version = parseVersion([1, 2, 3, 4]) , then the output will be:
working with version: Version.version1if, version = parseVersion([3, 2, 3, 4]) , then the output will be:
error parsing header: ResultException: invalid versionand, if version = parseVersion([]) , then the output will be:
error parsing header: ResultException: invalid header lengthSee more examples in example and test folders.
Result is a sealed class, so you can exhaustively match it with switch . So if you just want to get the value of the result, prefer using Dart 3 pattern matching instead of the fold method:
final result = Result.ok(42);
final value = switch (result) {
Ok(ok: final value) => value,
Err(err: final error) => 0,
};Multiple factory constructors are available to create a Result :
Result.ok(42);
Ok(42);
Result.success(42);are equivalent, and
Result.err('error');
Err('error');
Result.error('error');
Result.failure('error');are equivalent too.
All Result methods can be used with Future thanks to the extension methods:
final Future<Result<int, Exception>> result = Result.fromAsync(() => Future.value(42));
final Future<Result<int, Exception>> mappedResult = result.map((value) => value * 2);This project uses Just for managing tasks, so you can run tests with:
just testand generate coverage with:
just coverage