Author: Petrus Nguyễn Thái Học
Either monad for Dart language and Flutter framework.
The library for error handling and railway oriented programming.
Supports Monad comprehensions
(both sync
and async
versions).
Supports async map
and async flatMap
hiding the boilerplate of working with asynchronous computations Future<Either<L, R>>
.
Error handler library for type-safe and easy work with errors on Dart and Flutter.
Either is an alternative to Nullable value and Exceptions.
Credits: port and adapt from Λrrow-kt.
Liked some of my work? Buy me a coffee (or more likely a beer)
I have seen a lot of people importing whole libraries such as dartz and fpdart, ...
but they only use Either
class :). So I decided to write, port and adapt Either
class from Λrrow-kt.
- Inspired by Λrrow-kt, Scala Cats.
- Fully documented, tested and many examples. Every method/function in this library is documented with examples.
- This library is most complete
Either
implementation, which supportsMonad comprehensions
(bothsync
andasync
versions), and supportsasync map
andasync flatMap
hiding the boilerplate of working with asynchronous computationsFuture<Either<L, R>>
. - Very lightweight and simple library (compare to dartz).
In your Dart/Flutter project, add the dependency to your pubspec.yaml
dependencies:
dart_either: ^2.0.0
- Documentation: https://pub.dev/documentation/dart_either/latest/dart_either/dart_either-library.html
- Example: https://github.com/hoc081098/dart_either/tree/master/example/lib
- Flutter Example: https://github.com/hoc081098/node-auth-flutter-BLoC-pattern-RxDart
Either
is a type that represents either Right
(usually represent a "desired" value)
or Left
(usually represent a "undesired" value or error value).
Click to expand
In day-to-day programming, it is fairly common to find ourselves writing functions that can fail.
For instance, querying a service may result in a connection issue, or some unexpected JSON
response.
To communicate these errors, it has become common practice to throw exceptions; however, exceptions are not tracked in any way, shape, or form by the compiler. To see what kind of exceptions (if any) a function may throw, we have to dig through the source code. Then, to handle these exceptions, we have to make sure we catch them at the call site. This all becomes even more unwieldy when we try to compose exception-throwing procedures.
double throwsSomeStuff(int i) => throw UnimplementedError();
///
String throwsOtherThings(double d) => throw UnimplementedError();
///
List<int> moreThrowing(String s) => throw UnimplementedError();
///
List<int> magic(int i) => moreThrowing( throwsOtherThings( throwsSomeStuff(i) ) );
Assume we happily throw exceptions in our code. Looking at the types of the functions above,
any could throw a number of exceptions -- we do not know. When we compose, exceptions from any of the constituent
functions can be thrown. Moreover, they may throw the same kind of exception
(e.g., ArgumentError
) and, thus, it gets tricky tracking exactly where an exception came from.
How then do we communicate an error? By making it explicit in the data type we return.
Either
is used to short-circuit a computation upon the first error.
By convention, the right side of an Either
is used to hold successful values.
Because Either
is right-biased, it is possible to define a Monad
instance for it.
Since we only ever want the computation to continue in the case of Right
(as captured by the right-bias nature),
we fix the left type parameter and leave the right one free. So, the map
and flatMap
methods are right-biased.
Example:
/// Create an instance of [Right]
final right = Either<String, int>.right(10); // Either.Right(10)
/// Create an instance of [Left]
final left = Either<String, int>.left('none'); // Either.Left(none)
/// Map the right value to a [String]
final mapRight = right.map((a) => 'String: $a'); // Either.Right(String: 10)
/// Map the left value to a [int]
final mapLeft = right.mapLeft((a) => a.length); // Either.Right(10)
/// Return [Left] if the function throws an error.
/// Otherwise return [Right].
final catchError = Either.catchError(
(e, s) => 'Error: $e',
() => int.parse('invalid'),
);
// Either.Left(Error: FormatException: Invalid radix-10 number (at character 1)
// invalid
// ^
// )
/// Extract the value from [Either]
final value1 = right.getOrElse(() => -1); // 10
final value2 = right.getOrHandle((l) => -1); // 10
/// Chain computations
final flatMap = right.flatMap((a) => Either.right(a + 10)); // Either.Right(20)
/// Pattern matching
right.fold(
ifLeft: (l) => print('Left value: $l'),
ifRight: (r) => print('Right value: $r'),
); // Right: 10
right.when(
ifLeft: (l) => print('Left: $l'),
ifRight: (r) => print('Right: $r'),
); // Prints Right: Either.Right(10)
// Or use Dart 3.0 switch expression syntax 🤘
print(
switch (right) {
Left() => 'Left: $right',
Right() => 'Right: $right',
},
); // Prints Right: Either.Right(10)
/// Convert to nullable value
final nullableValue = right.orNull(); // 10
Use - Documentation
// Left('Left value')
final left = Either<Object, String>.left('Left value'); // or Left<Object, String>('Left value');
// Right(1)
final right = Either<Object, int>.right(1); // or Right<Object, int>(1);
// Left('Left value')
Either<Object, String>.binding((e) {
final String s = left.bind(e);
final int i = right.bind(e);
return '$s $i';
});
// Left(FormatException(...))
Either.catchError(
(e, s) => 'Error: $e',
() => int.parse('invalid'),
);
- Either.catchFutureError
- Either.catchStreamError
- Either.fromNullable
- Either.futureBinding
- Either.parSequenceN
- Either.parTraverseN
- Either.sequence
- Either.traverse
import 'package:http/http.dart' as http;
/// Either.catchFutureError
Future<Either<String, http.Response>> eitherFuture = Either.catchFutureError(
(e, s) => 'Error: $e',
() async {
final uri = Uri.parse('https://pub.dev/packages/dart_either');
return http.get(uri);
},
);
(await eitherFuture).fold(ifLeft: print, ifRight: print);
/// Either.catchStreamError
Stream<int> genStream() async* {
for (var i = 0; i < 5; i++) {
yield i;
}
throw Exception('Fatal');
}
Stream<Either<String, int>> eitherStream = Either.catchStreamError(
(e, s) => 'Error: $e',
genStream(),
);
eitherStream.listen(print);
/// Either.fromNullable
Either.fromNullable<int>(null); // Left(null)
Either.fromNullable<int>(1); // Right(1)
/// Either.futureBinding
String url1 = 'url1';
String url2 = 'url2';
Either.futureBinding<String, http.Response>((e) async {
final response = await Either.catchFutureError(
(e, s) => 'Get $url1: $e',
() async {
final uri = Uri.parse(url1);
return http.get(uri);
},
).bind(e);
final id = Either.catchError(
(e, s) => 'Parse $url1 body: $e',
() => jsonDecode(response.body)['id'] as String,
).bind(e);
return await Either.catchFutureError(
(e, s) => 'Get $url2: $e',
() async {
final uri = Uri.parse('$url2?id=$id');
return http.get(uri);
},
).bind(e);
});
/// Either.sequence
List<Either<String, http.Response>> eithers = await Future.wait(
[1, 2, 3, 4, 5].map((id) {
final url = 'url?id=$id';
return Either.catchFutureError(
(e, s) => 'Get $url: $e',
() async {
final uri = Uri.parse(url);
return http.get(uri);
},
);
}),
);
Either<String, BuiltList<http.Response>> result = Either.sequence(eithers);
/// Either.traverse
Either<String, BuiltList<Uri>> urisEither = Either.traverse(
['url1', 'url2', '::invalid::'],
(String uriString) => Either.catchError(
(e, s) => 'Failed to parse $uriString: $e',
() => Uri.parse(uriString),
),
); // Left(FormatException('Failed to parse ::invalid:::...'))
/// Stream.toEitherStream
Stream<int> genStream() async* {
for (var i = 0; i < 5; i++) {
yield i;
}
throw Exception('Fatal');
}
Stream<Either<String, int>> eitherStream = genStream().toEitherStream((e, s) => 'Error: $e');
eitherStream.listen(print);
/// Future.toEitherFuture
Future<Either<Object, int>> f1 = Future<int>.error('An error').toEitherFuture((e, s) => e);
Future<Either<Object, int>> f2 = Future<int>.value(1).toEitherFuture((e, s) => e);
await f1; // Left('An error')
await f2; // Right(1)
/// T.left, T.right
Either<int, String> left = 1.left<String>();
Either<String, int> right = 2.right<String>();
- isLeft
- isRight
- fold
- foldLeft
- swap
- tapLeft
- tap
- map
- mapLeft
- flatMap
- bimap
- exists
- all
- getOrElse
- orNull
- getOrHandle
- findOrNull
- when
- handleErrorWith
- handleError
- redeem
- redeemWith
- toFuture
- getOrThrow
Future<Either<AsyncError, dynamic>> httpGetAsEither(String uriString) {
Either<AsyncError, dynamic> toJson(http.Response response) =>
response.statusCode >= 200 && response.statusCode < 300
? Either<AsyncError, dynamic>.catchError(
toAsyncError,
() => jsonDecode(response.body),
)
: AsyncError(
HttpException(
'statusCode=${response.statusCode}, body=${response.body}',
uri: response.request?.url,
),
StackTrace.current,
).left<dynamic>();
Future<Either<AsyncError, http.Response>> httpGet(Uri uri) =>
Either.catchFutureError(toAsyncError, () => http.get(uri));
final uri =
Future.value(Either.catchError(toAsyncError, () => Uri.parse(uriString)));
return uri.thenFlatMapEither(httpGet).thenFlatMapEither<dynamic>(toJson);
}
Either<AsyncError, BuiltList<User>> toUsers(List list) { ... }
Either<AsyncError, BuiltList<User>> result = await httpGetAsEither('https://jsonplaceholder.typicode.com/users')
.thenMapEither((dynamic json) => json as List)
.thenFlatMapEither(toUsers);
You can use Monad comprehensions
via Either.binding
and Either.futureBinding
.
Future<Either<AsyncError, dynamic>> httpGetAsEither(String uriString) =>
Either.futureBinding<AsyncError, dynamic>((e) async {
final uri =
Either.catchError(toAsyncError, () => Uri.parse(uriString)).bind(e);
final response = await Either.catchFutureError(
toAsyncError,
() => http.get(uri),
).bind(e);
e.ensure(
response.statusCode >= 200 && response.statusCode < 300,
() => AsyncError(
HttpException(
'statusCode=${response.statusCode}, body=${response.body}',
uri: response.request?.url,
),
StackTrace.current,
),
);
return Either<AsyncError, dynamic>.catchError(
toAsyncError, () => jsonDecode(response.body)).bind(e);
});
Either<AsyncError, BuiltList<User>> toUsers(List list) { ... }
Either<AsyncError, BuiltList<User>> result = await Either.futureBinding((e) async {
final dynamic json = await httpGetAsEither('https://jsonplaceholder.typicode.com/users').bind(e);
final BuiltList<User> users = toUsers(json as List).bind(e);
return users;
});
Please file feature requests and bugs at the issue tracker.
MIT License
Copyright (c) 2021-2024 Petrus Nguyễn Thái Học