dart_either

Creator: coderz1093

Last updated:

Add to Cart

Description:

dart either

dart_either #
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)

Difference from other implementations (dartz and fpdart) #
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 supports Monad comprehensions (both sync and async versions),
and supports async map and async flatMap hiding the boilerplate of working with asynchronous computations Future<Either<L, R>>.
Very lightweight and simple library (compare to dartz).

Getting started #
In your Dart/Flutter project, add the dependency to your pubspec.yaml
dependencies:
dart_either: ^2.0.0
copied to clipboard
Documentation & example #

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 monad #
Either is a type that represents either Right (usually represent a "desired" value)
or Left (usually represent a "undesired" value or error value).

Elm Result.
Haskell Data.Either.
Rust Result.


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) ) );
copied to clipboard
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
copied to clipboard

Use - Documentation #
1. Creation #
1.1. Factory constructors

Either.left
Either.right
Either.binding
Either.catchError
Left
Right

// 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'),
);
copied to clipboard
1.2. Static methods

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:::...'))
copied to clipboard
1.3. Extension methods

Stream.toEitherStream
Future.toEitherFuture
T.left
T.right

/// 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>();
copied to clipboard
2. Operations #

isLeft
isRight
fold
foldLeft
swap
tapLeft
tap
map
mapLeft
flatMap
bimap
exists
all
getOrElse
orNull
getOrHandle
findOrNull
when
handleErrorWith
handleError
redeem
redeemWith
toFuture
getOrThrow

3. Extensions on Future<Either<L, R>>. #

thenFlatMapEither
thenMapEither

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);
copied to clipboard
4. Monad comprehensions #
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;
});
copied to clipboard
References #

Functional Error Handling
Monad
Monad Comprehensions

Features and bugs #
Please file feature requests and bugs at the issue tracker.
License #
MIT License

Copyright (c) 2021-2024 Petrus Nguyễn Thái Học
copied to clipboard

License

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Files:

Customer Reviews

There are no reviews.