Skip to content

Commit

Permalink
Null Safety interoperability (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
SandroMaglione authored Oct 24, 2022
2 parents e844fd9 + 758976e commit 80dac46
Show file tree
Hide file tree
Showing 17 changed files with 509 additions and 5 deletions.
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
# v0.4.0 - Soon
- Added extension methods to work with nullable types (`T?`)
- From `T?` to `fpdart`'s types
- `toOption`
- `toEither`
- `toTaskOption`
- `toIOEither`
- `toTaskEither`
- `toTaskEitherAsync`
- `fromNullable` (`Either`, `IOEither`, `TaskOption` `TaskEither`)
- `fromNullableAsync` (`TaskEither`)
- From `fpdart`'s types to `T?`
- `toNullable` (`Either`)
```dart
/// [Option] <-> `int?`
int? value1 = 10.toOption().map((t) => t + 10).toNullable();
bool? value2 = value1?.isEven;
/// `bool?` -> [Either] -> `int?`
int? value3 = value2
.toEither(() => "Error")
.flatMap((a) => a ? right<String, int>(10) : left<String, int>("None"))
.toNullable();
/// `int?` -> [Option]
Option<int> value4 = (value3?.abs().round()).toOption().flatMap(Option.of);
```
- Added `toIOEither` to `Either`
- Removed parameter from `Either` `fromNullable` [⚠️ **BREAKING CHANGE**]
```dart
final either = Either<String, int>.fromNullable(value, (r) => 'none');
/// 👆 Removed the value `(r)` (it was always null anyway 💁🏼‍♂️) 👇
final either = Either<String, int>.fromNullable(value, () => 'none');
```
- Added article about [Option type and Null Safety in dart](https://www.sandromaglione.com/techblog/option_type_and_null_safety_dart)

# v0.3.0 - 11 October 2022
- Inverted `onSome` and `onNone` functions parameters in `match` method of `Option` [⚠️ **BREAKING CHANGE**] (*Read more on why* 👉 [#56](https://github.com/SandroMaglione/fpdart/pull/56))
```dart
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Check out also this series of articles about functional programming with `fpdart
- [How to make API requests with validation in fpdart](https://www.sandromaglione.com/techblog/fpdart-api-request-with-validation-functional-programming)
- [How to use TaskEither in fpdart](https://www.sandromaglione.com/techblog/how-to-use-task-either-fpdart-functional-programming)
- [How to map an Either to a Future in fpdart](https://blog.sandromaglione.com/techblog/from-sync-to-async-functional-programming)
- [Option type and Null Safety in dart](https://www.sandromaglione.com/techblog/option_type_and_null_safety_dart)

## 💻 Installation

Expand Down
2 changes: 1 addition & 1 deletion example/src/either/chain_either.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Future<void> placeOrder() async {
/// Same code using fpart and Functional programming
Either.fromNullable(
authRepository.currentUser?.uid,
(r) => 'Missing uid',
() => 'Missing uid',
).toTaskEither().flatMap(
(uid) => TaskEither.tryCatch(
() async => cartRepository.fetchCart(uid),
Expand Down
43 changes: 43 additions & 0 deletions example/src/option/nullable/chain_nullable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'dart:math';

import 'package:fpdart/fpdart.dart';

int? nullable() => Random().nextBool() ? 10 : null;

void main(List<String> args) {
/// [Option] <-> `int?`
int? value1 = 10.toOption().map((t) => t + 10).toNullable();

bool? value2 = value1?.isEven;

/// `bool?` -> [Either] -> `int?`
int? value3 = value2
.toEither(() => "Error")
.flatMap((a) => a ? right<String, int>(10) : left<String, int>("None"))
.toNullable();

/// `int?` -> [Option]
Option<int> value4 = (value3?.abs().round()).toOption().flatMap(Option.of);

Option<int> value = (10
.toOption()
.map((t) => t + 10)
.toNullable()

/// Null safety 🎯
?.ceil()

/// Null safety 🎯
.isEven
.toEither(() => "Error")
.flatMap((a) => right<String, int>(10))
.toNullable()

/// Null safety 🎯
?.abs()

/// Null safety 🎯
.round())
.toOption()
.flatMap(Option.of);
}
64 changes: 64 additions & 0 deletions example/src/option/nullable/option_nullable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// ignore_for_file: unchecked_use_of_nullable_value, undefined_getter
import 'package:fpdart/fpdart.dart';
import 'package:glados/glados.dart';

int doSomething(String str) => str.length + 10 * 2;
int doSomethingElse(int number) => number + 10 * 2;

void main(List<String> args) {
int? nullableInt = 10;
if (nullableInt == null) {
print("Missing ‼️");
} else {
print("Found $nullableInt 🎯");
}

/// 👆 Exactly the same as 👇
Option<int> optionInt = Option.of(10);
optionInt.match(() {
print("Missing ‼️");
}, (t) {
print("Found $nullableInt 🎯");
});

/// Null safety and `Option` save you from `null` 🚀
String? str = Random().nextBool() ? "string" : null;
Option<String> optionStr = Random().nextBool() ? some("string") : none();

/// ⛔️ The property 'toLowerCase' can't be unconditionally accessed because the receiver can be 'null'.
str.toLowerCase;

/// ⛔️ The getter 'toLowerCase' isn't defined for the type 'Option<String>'.
optionStr.toLowerCase;

/// Option has methods that makes it more powerful (chain methods) ⛓
String? strNullable = Random().nextBool() ? "string" : null;
Option<String> optionNullable = some("string");

/// Declarative API: more readable and composable 🎉
Option<double> optionIntNullable = optionNullable
.map(doSomething)
.alt(() => some(20))
.map(doSomethingElse)
.flatMap((t) => some(t / 2));

/// Not really clear what is going on here 🤔
double? intNullable = (strNullable != null
? doSomethingElse(doSomething(strNullable))
: doSomethingElse(20)) /
2;

if (optionNullable.isSome()) {
/// Still type `Option<int>`, not `Some<int>` 😐
optionIntNullable;
}

if (strNullable != null) {
/// This is now `String` 🤝
strNullable;
}

List<int>? list = Random().nextBool() ? [1, 2, 3, 4] : null;
list.map((e) => /** What type is `e`? 😐 */ null);
}
46 changes: 46 additions & 0 deletions example/src/option/nullable/overview.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'dart:math';

import 'package:fpdart/fpdart.dart';

int? nullable() => Random().nextBool() ? 10 : null;

String takesNullable(int? nullInt) => "$nullInt";

void main(List<String> args) {
int noNull = 10;
int? canBeNull = nullable();

/// `bool`
final noNullIsEven = noNull.isEven;

/// final canBeNullIsEven = canBeNull.isEven; ⛔️
/// `bool?`
final canBeNullIsEven = canBeNull?.isEven;

/// ☑️
takesNullable(canBeNull);

/// ☑️
takesNullable(noNull);

/// ☑️
noNull.abs();

/// ☑️
canBeNull?.abs();

Option<int> optionInt = Option.of(10);
int? nullInt = nullable();

nullInt?.abs();
optionInt.map((t) => t.abs());

nullInt?.isEven;
optionInt.map((t) => t.isEven);

takesNullable(nullInt);

/// takesNullable(optionInt); ⛔️
takesNullable(optionInt.toNullable());
}
1 change: 1 addition & 0 deletions lib/fpdart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export 'src/io_either.dart';
export 'src/io_ref.dart';
export 'src/list_extension.dart';
export 'src/map_extension.dart';
export 'src/nullable_extension.dart';
export 'src/option.dart';
export 'src/predicate.dart';
export 'src/random.dart';
Expand Down
26 changes: 24 additions & 2 deletions lib/src/either.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'function.dart';
import 'io_either.dart';
import 'option.dart';
import 'task_either.dart';
import 'tuple.dart';
Expand Down Expand Up @@ -195,13 +196,22 @@ abstract class Either<L, R> extends HKT2<_EitherHKT, L, R>
/// - If the [Either] is [Right], return a [Some] containing the value inside [Right]
Option<R> toOption();

/// Convert this [Either] to a [IOEither].
IOEither<L, R> toIOEither();

/// Convert this [Either] to a [TaskEither].
///
/// Used to convert a sync context ([Either]) to an async context ([TaskEither]).
/// You should convert [Either] to [TaskEither] every time you need to
/// call an async ([Future]) function based on the value in [Either].
TaskEither<L, R> toTaskEither();

/// Convert [Either] to nullable `R?`.
///
/// **Note**: this loses information about a possible [Left] value,
/// converting it to simply `null`.
R? toNullable();

/// Return `true` when this [Either] is [Left].
bool isLeft();

Expand Down Expand Up @@ -387,8 +397,8 @@ abstract class Either<L, R> extends HKT2<_EitherHKT, L, R>

/// If `r` is `null`, then return the result of `onNull` in [Left].
/// Otherwise return `Right(r)`.
factory Either.fromNullable(R? r, L Function(R? r) onNull) =>
r != null ? Either.of(r) : Either.left(onNull(r));
factory Either.fromNullable(R? r, L Function() onNull) =>
r != null ? Either.of(r) : Either.left(onNull());

/// Try to execute `run`. If no error occurs, then return [Right].
/// Otherwise return [Left] containing the result of `onError`.
Expand Down Expand Up @@ -517,6 +527,12 @@ class Right<L, R> extends Either<L, R> {

@override
TaskEither<L, R> toTaskEither() => TaskEither.of(_value);

@override
IOEither<L, R> toIOEither() => IOEither.of(_value);

@override
R? toNullable() => _value;
}

class Left<L, R> extends Either<L, R> {
Expand Down Expand Up @@ -600,4 +616,10 @@ class Left<L, R> extends Either<L, R> {

@override
TaskEither<L, R> toTaskEither() => TaskEither.left(_value);

@override
IOEither<L, R> toIOEither() => IOEither.left(_value);

@override
R? toNullable() => null;
}
5 changes: 5 additions & 0 deletions lib/src/io_either.dart
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ class IOEither<L, R> extends HKT2<_IOEitherHKT, L, R>
/// Build a [IOEither] that returns `either`.
factory IOEither.fromEither(Either<L, R> either) => IOEither(() => either);

/// If `r` is `null`, then return the result of `onNull` in [Left].
/// Otherwise return `Right(r)`.
factory IOEither.fromNullable(R? r, L Function() onNull) =>
Either.fromNullable(r, onNull).toIOEither();

/// Converts a [Function] that may throw to a [Function] that never throws
/// but returns a [Either] instead.
///
Expand Down
46 changes: 46 additions & 0 deletions lib/src/nullable_extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'either.dart';
import 'io_either.dart';
import 'option.dart';
import 'task.dart';
import 'task_either.dart';
import 'task_option.dart';

/// `fpdart` extension to work on nullable values `T?`
extension FpdartOnNullable<T> on T? {
/// Convert a nullable type `T?` to [Option]:
/// {@template fpdart_on_nullable_to_option}
/// - [Some] if the value is **not** `null`
/// - [None] if the value is `null`
/// {@endtemplate}
Option<T> toOption() => Option.fromNullable(this);

/// Convert a nullable type `T?` to [Either]:
/// {@template fpdart_on_nullable_to_either}
/// - [Right] if the value is **not** `null`
/// - [Left] containing the result of `onNull` if the value is `null`
/// {@endtemplate}
Either<L, T> toEither<L>(L Function() onNull) =>
Either.fromNullable(this, onNull);

/// Convert a nullable type `T?` to [TaskOption]:
/// {@macro fpdart_on_nullable_to_option}
TaskOption<T> toTaskOption() => TaskOption.fromNullable(this);

/// Convert a nullable type `T?` to [IOEither].
IOEither<L, T> toIOEither<L>(L Function() onNull) =>
IOEither.fromNullable(this, onNull);

/// Convert a nullable type `T?` to [TaskEither] using a **sync function**:
/// {@macro fpdart_on_nullable_to_either}
///
/// If you want to run an **async** function `onNull`, use `toTaskEitherAsync`.
TaskEither<L, T> toTaskEither<L>(L Function() onNull) =>
TaskEither.fromNullable(this, onNull);

/// Convert a nullable type `T?` to [TaskEither] using an **async function**:
/// {@macro fpdart_on_nullable_to_either}
///
/// If you want to run an **sync** function `onNull`, use `toTaskEither`.
TaskEither<L, T> toTaskEitherAsync<L>(Task<L> onNull) =>
TaskEither.fromNullableAsync(this, onNull);
}
11 changes: 11 additions & 0 deletions lib/src/task_either.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@ class TaskEither<L, R> extends HKT2<_TaskEitherHKT, L, R>
factory TaskEither.fromTask(Task<R> task) =>
TaskEither(() async => Right(await task.run()));

/// {@template fpdart_from_nullable_task_either}
/// If `r` is `null`, then return the result of `onNull` in [Left].
/// Otherwise return `Right(r)`.
/// {@endtemplate}
factory TaskEither.fromNullable(R? r, L Function() onNull) =>
Either.fromNullable(r, onNull).toTaskEither();

/// {@macro fpdart_from_nullable_task_either}
factory TaskEither.fromNullableAsync(R? r, Task<L> onNull) => TaskEither(
() async => r != null ? Either.of(r) : Either.left(await onNull.run()));

/// When calling `predicate` with `value` returns `true`, then running [TaskEither] returns `Right(value)`.
/// Otherwise return `onFalse`.
factory TaskEither.fromPredicate(
Expand Down
5 changes: 5 additions & 0 deletions lib/src/task_option.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ class TaskOption<R> extends HKT<_TaskOptionHKT, R>
factory TaskOption.fromTask(Task<R> task) =>
TaskOption(() async => Option.of(await task.run()));

/// If `r` is `null`, then return [None].
/// Otherwise return `Right(r)`.
factory TaskOption.fromNullable(R? r) =>
Option.fromNullable(r).toTaskOption();

/// When calling `predicate` with `value` returns `true`, then running [TaskOption] returns `Some(value)`.
/// Otherwise return [None].
factory TaskOption.fromPredicate(R value, bool Function(R a) predicate) =>
Expand Down
Loading

0 comments on commit 80dac46

Please sign in to comment.