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.ok
to get the value if the result is successful and discard the error if it's a failureresult.err
to get the error if the result is a failure and discard the value if it's a successresult.stackTrace
to 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.isOk
to check if the result is successfulresult.isErr
to 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 == result2
to check if two results are equalresult1 != result2
to 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 asmapOrElse
with a different syntaxresult.foldAsync<U>(FutureOr<U> Function(value) okFn, FutureOr<U> Function(error) errFn)
same asmapOrElseAsync
with 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 & result2
to combine two results with theand
operatorresult1 | result2
to combine two results with theor
operator
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.version1
if, version = parseVersion([3, 2, 3, 4])
, then the output will be:
error parsing header: ResultException: invalid version
and, if version = parseVersion([])
, then the output will be:
error parsing header: ResultException: invalid header length
See 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 test
and generate coverage with:
just coverage