diff --git a/CHANGELOG.md b/CHANGELOG.md index dac3a1fe..7c9334d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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(10) : left("None")) + .toNullable(); + +/// `int?` -> [Option] +Option value4 = (value3?.abs().round()).toOption().flatMap(Option.of); +``` +- Added `toIOEither` to `Either` +- Removed parameter from `Either` `fromNullable` [⚠️ **BREAKING CHANGE**] +```dart +final either = Either.fromNullable(value, (r) => 'none'); + +/// 👆 Removed the value `(r)` (it was always null anyway 💁🏼‍♂️) 👇 + +final either = Either.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 diff --git a/README.md b/README.md index 7ae52343..f66b9124 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/example/src/either/chain_either.dart b/example/src/either/chain_either.dart index 6f7b66c9..0dca6499 100644 --- a/example/src/either/chain_either.dart +++ b/example/src/either/chain_either.dart @@ -57,7 +57,7 @@ Future 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), diff --git a/example/src/option/nullable/chain_nullable.dart b/example/src/option/nullable/chain_nullable.dart new file mode 100644 index 00000000..17229ad1 --- /dev/null +++ b/example/src/option/nullable/chain_nullable.dart @@ -0,0 +1,43 @@ +import 'dart:math'; + +import 'package:fpdart/fpdart.dart'; + +int? nullable() => Random().nextBool() ? 10 : null; + +void main(List 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(10) : left("None")) + .toNullable(); + + /// `int?` -> [Option] + Option value4 = (value3?.abs().round()).toOption().flatMap(Option.of); + + Option value = (10 + .toOption() + .map((t) => t + 10) + .toNullable() + + /// Null safety 🎯 + ?.ceil() + + /// Null safety 🎯 + .isEven + .toEither(() => "Error") + .flatMap((a) => right(10)) + .toNullable() + + /// Null safety 🎯 + ?.abs() + + /// Null safety 🎯 + .round()) + .toOption() + .flatMap(Option.of); +} diff --git a/example/src/option/nullable/option_nullable.dart b/example/src/option/nullable/option_nullable.dart new file mode 100644 index 00000000..5a2072af --- /dev/null +++ b/example/src/option/nullable/option_nullable.dart @@ -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 args) { + int? nullableInt = 10; + if (nullableInt == null) { + print("Missing ‼️"); + } else { + print("Found $nullableInt 🎯"); + } + + /// 👆 Exactly the same as 👇 + + Option 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 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'. + optionStr.toLowerCase; + + /// Option has methods that makes it more powerful (chain methods) ⛓ + String? strNullable = Random().nextBool() ? "string" : null; + Option optionNullable = some("string"); + + /// Declarative API: more readable and composable 🎉 + Option 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`, not `Some` 😐 + optionIntNullable; + } + + if (strNullable != null) { + /// This is now `String` 🤝 + strNullable; + } + + List? list = Random().nextBool() ? [1, 2, 3, 4] : null; + list.map((e) => /** What type is `e`? 😐 */ null); +} diff --git a/example/src/option/nullable/overview.dart b/example/src/option/nullable/overview.dart new file mode 100644 index 00000000..102d7c2d --- /dev/null +++ b/example/src/option/nullable/overview.dart @@ -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 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 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()); +} diff --git a/lib/fpdart.dart b/lib/fpdart.dart index 12a259fb..5cf69518 100644 --- a/lib/fpdart.dart +++ b/lib/fpdart.dart @@ -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'; diff --git a/lib/src/either.dart b/lib/src/either.dart index 74efff3b..d7e2b391 100644 --- a/lib/src/either.dart +++ b/lib/src/either.dart @@ -1,4 +1,5 @@ import 'function.dart'; +import 'io_either.dart'; import 'option.dart'; import 'task_either.dart'; import 'tuple.dart'; @@ -195,6 +196,9 @@ abstract class Either extends HKT2<_EitherHKT, L, R> /// - If the [Either] is [Right], return a [Some] containing the value inside [Right] Option toOption(); + /// Convert this [Either] to a [IOEither]. + IOEither toIOEither(); + /// Convert this [Either] to a [TaskEither]. /// /// Used to convert a sync context ([Either]) to an async context ([TaskEither]). @@ -202,6 +206,12 @@ abstract class Either extends HKT2<_EitherHKT, L, R> /// call an async ([Future]) function based on the value in [Either]. TaskEither 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(); @@ -387,8 +397,8 @@ abstract class Either 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`. @@ -517,6 +527,12 @@ class Right extends Either { @override TaskEither toTaskEither() => TaskEither.of(_value); + + @override + IOEither toIOEither() => IOEither.of(_value); + + @override + R? toNullable() => _value; } class Left extends Either { @@ -600,4 +616,10 @@ class Left extends Either { @override TaskEither toTaskEither() => TaskEither.left(_value); + + @override + IOEither toIOEither() => IOEither.left(_value); + + @override + R? toNullable() => null; } diff --git a/lib/src/io_either.dart b/lib/src/io_either.dart index 8f59204f..407c0caa 100644 --- a/lib/src/io_either.dart +++ b/lib/src/io_either.dart @@ -210,6 +210,11 @@ class IOEither extends HKT2<_IOEitherHKT, L, R> /// Build a [IOEither] that returns `either`. factory IOEither.fromEither(Either 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. /// diff --git a/lib/src/nullable_extension.dart b/lib/src/nullable_extension.dart new file mode 100644 index 00000000..093d5c59 --- /dev/null +++ b/lib/src/nullable_extension.dart @@ -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 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 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 toEither(L Function() onNull) => + Either.fromNullable(this, onNull); + + /// Convert a nullable type `T?` to [TaskOption]: + /// {@macro fpdart_on_nullable_to_option} + TaskOption toTaskOption() => TaskOption.fromNullable(this); + + /// Convert a nullable type `T?` to [IOEither]. + IOEither toIOEither(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 toTaskEither(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 toTaskEitherAsync(Task onNull) => + TaskEither.fromNullableAsync(this, onNull); +} diff --git a/lib/src/task_either.dart b/lib/src/task_either.dart index d8d460b8..1f14bc3b 100644 --- a/lib/src/task_either.dart +++ b/lib/src/task_either.dart @@ -183,6 +183,17 @@ class TaskEither extends HKT2<_TaskEitherHKT, L, R> factory TaskEither.fromTask(Task 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 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( diff --git a/lib/src/task_option.dart b/lib/src/task_option.dart index 981cc59c..0789abde 100644 --- a/lib/src/task_option.dart +++ b/lib/src/task_option.dart @@ -150,6 +150,11 @@ class TaskOption extends HKT<_TaskOptionHKT, R> factory TaskOption.fromTask(Task 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) => diff --git a/test/src/either_test.dart b/test/src/either_test.dart index ec1379a7..31b7a900 100644 --- a/test/src/either_test.dart +++ b/test/src/either_test.dart @@ -436,6 +436,36 @@ void main() { }); }); + group('toNullable', () { + test('Right', () { + final value = Either.of(10); + final ap = value.toNullable(); + expect(ap, 10); + }); + + test('Left', () { + final value = Either.left('none'); + final ap = value.toNullable(); + expect(ap, null); + }); + }); + + group('toIOEither', () { + test('Right', () { + final value = Either.of(10); + final ap = value.toIOEither(); + final result = ap.run(); + expect(result, value); + }); + + test('Left', () { + final value = Either.left('none'); + final ap = value.toIOEither(); + final result = ap.run(); + expect(result, value); + }); + }); + group('toTaskEither', () { test('Right', () async { final value = Either.of(10); @@ -751,14 +781,14 @@ void main() { group('fromNullable', () { test('Right', () { - final either = Either.fromNullable(10, (r) => 'none'); + final either = Either.fromNullable(10, () => 'none'); either.match((_) { fail('should be right'); }, (r) => expect(r, 10)); }); test('Left', () { - final either = Either.fromNullable(null, (r) => 'none'); + final either = Either.fromNullable(null, () => 'none'); either.match((l) => expect(l, 'none'), (_) { fail('should be left'); }); diff --git a/test/src/io_either_test.dart b/test/src/io_either_test.dart index 548ff6c2..360a6700 100644 --- a/test/src/io_either_test.dart +++ b/test/src/io_either_test.dart @@ -370,6 +370,24 @@ void main() { }); }); + group('fromNullable', () { + test('Right', () { + final task = IOEither.fromNullable(10, () => "Error"); + final result = task.run(); + result.matchTestRight((r) { + expect(r, 10); + }); + }); + + test('Left', () { + final task = IOEither.fromNullable(null, () => "Error"); + final result = task.run(); + result.matchTestLeft((l) { + expect(l, "Error"); + }); + }); + }); + group('fromPredicate', () { test('True', () { final task = diff --git a/test/src/nullable_extension_test.dart b/test/src/nullable_extension_test.dart new file mode 100644 index 00000000..cc69dc35 --- /dev/null +++ b/test/src/nullable_extension_test.dart @@ -0,0 +1,119 @@ +import 'package:fpdart/fpdart.dart'; + +import 'utils/utils.dart'; + +void main() { + group("FpdartOnNullable", () { + group("toOption", () { + test('Some', () { + final value = 10; + final result = value.toOption(); + result.matchTestSome((t) { + expect(t, value); + }); + }); + + test('None', () { + int? value = null; + final result = value.toOption(); + expect(result, isA>()); + }); + }); + + group("toEither", () { + test('Right', () { + final value = 10; + final result = value.toEither(() => "Error"); + result.matchTestRight((t) { + expect(t, value); + }); + }); + + test('Left', () { + int? value = null; + final result = value.toEither(() => "Error"); + result.matchTestLeft((l) { + expect(l, "Error"); + }); + }); + }); + + group("toTaskOption", () { + test('Right', () async { + final value = 10; + final task = value.toTaskOption(); + final result = await task.run(); + result.matchTestSome((t) { + expect(t, value); + }); + }); + + test('Left', () async { + int? value = null; + final task = value.toTaskOption(); + final result = await task.run(); + expect(result, isA>()); + }); + }); + + group("toIOEither", () { + test('Right', () { + final value = 10; + final task = value.toIOEither(() => "Error"); + final result = task.run(); + result.matchTestRight((t) { + expect(t, value); + }); + }); + + test('Left', () { + int? value = null; + final task = value.toIOEither(() => "Error"); + final result = task.run(); + result.matchTestLeft((l) { + expect(l, "Error"); + }); + }); + }); + + group("toTaskEither", () { + test('Right', () async { + final value = 10; + final task = value.toTaskEither(() => "Error"); + final result = await task.run(); + result.matchTestRight((t) { + expect(t, value); + }); + }); + + test('Left', () async { + int? value = null; + final task = value.toTaskEither(() => "Error"); + final result = await task.run(); + result.matchTestLeft((l) { + expect(l, "Error"); + }); + }); + }); + + group("toTaskEitherAsync", () { + test('Right', () async { + final value = 10; + final task = value.toTaskEitherAsync(Task.of("Error")); + final result = await task.run(); + result.matchTestRight((t) { + expect(t, value); + }); + }); + + test('Left', () async { + int? value = null; + final task = value.toTaskEitherAsync(Task.of("Error")); + final result = await task.run(); + result.matchTestLeft((l) { + expect(l, "Error"); + }); + }); + }); + }); +} diff --git a/test/src/task_either_test.dart b/test/src/task_either_test.dart index fc50d208..0d4ea320 100644 --- a/test/src/task_either_test.dart +++ b/test/src/task_either_test.dart @@ -377,6 +377,44 @@ void main() { }); }); + group('fromNullable', () { + test('Right', () async { + final task = TaskEither.fromNullable(10, () => "Error"); + final result = await task.run(); + result.matchTestRight((r) { + expect(r, 10); + }); + }); + + test('Left', () async { + final task = TaskEither.fromNullable(null, () => "Error"); + final result = await task.run(); + result.matchTestLeft((l) { + expect(l, "Error"); + }); + }); + }); + + group('fromNullableAsync', () { + test('Right', () async { + final task = TaskEither.fromNullableAsync( + 10, Task(() async => "Error")); + final result = await task.run(); + result.matchTestRight((r) { + expect(r, 10); + }); + }); + + test('Left', () async { + final task = TaskEither.fromNullableAsync( + null, Task(() async => "Error")); + final result = await task.run(); + result.matchTestLeft((l) { + expect(l, "Error"); + }); + }); + }); + group('fromPredicate', () { test('True', () async { final task = TaskEither.fromPredicate( diff --git a/test/src/task_option_test.dart b/test/src/task_option_test.dart index dabcedbc..41d93cbb 100644 --- a/test/src/task_option_test.dart +++ b/test/src/task_option_test.dart @@ -210,6 +210,22 @@ void main() { }); }); + group('fromNullable', () { + test('Right', () async { + final task = TaskOption.fromNullable(10); + final result = await task.run(); + result.matchTestSome((r) { + expect(r, 10); + }); + }); + + test('Left', () async { + final task = TaskOption.fromNullable(null); + final result = await task.run(); + expect(result, isA>()); + }); + }); + group('fromPredicate', () { test('True', () async { final task = TaskOption.fromPredicate(20, (n) => n > 10);