From e70f577d789e2a4acc22a6f023b6f52f5616b96d Mon Sep 17 00:00:00 2001 From: Sandro Maglione Date: Mon, 5 Jul 2021 12:13:03 +0200 Subject: [PATCH] TaskOption --- CHANGELOG.md | 1 + README.md | 12 +- lib/fpdart.dart | 1 + lib/src/task_option.dart | 161 +++++++++++++++++ test/src/task_option_test.dart | 317 +++++++++++++++++++++++++++++++++ 5 files changed, 486 insertions(+), 6 deletions(-) create mode 100644 lib/src/task_option.dart create mode 100644 test/src/task_option_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 846816f2..4a7429cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Added `Compose` and `Compose2`, used to easily compose functions in a chain. - Added `curry` and `uncurry` extensions on functions up to 5 parameters. +- Completed `TaskOption` type implementation, documentation, and testing # v0.0.6 - 29 June 2021 diff --git a/README.md b/README.md index 3a69ff5f..46411b06 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Would you like to know more about functional programming, fpdart, and how to use - [x] `IO` - [x] `Iterable` (`List`) `extension` - [x] `IOEither` -- [ ] `TaskOption` +- [x] `TaskOption` - [ ] `ReaderEither` - [ ] `ReaderTask` - [ ] `ReaderTaskEither` @@ -61,7 +61,7 @@ Would you like to know more about functional programming, fpdart, and how to use ```yaml # pubspec.yaml dependencies: - fpdart: ^0.0.6 # Check out the latest version + fpdart: ^0.0.7 # Check out the latest version ``` ## ✨ Examples @@ -231,10 +231,10 @@ The roadmap for types development is highlighted below (breaking changes to _'st - ~~Implementation~~ - ~~Documentation~~ - ~~Testing~~ -11. `TaskOption` - - Implementation - - Documentation - - Testing +11. ~~`TaskOption`~~ + - ~~Implementation~~ + - ~~Documentation~~ + - ~~Testing~~ 12. `ReaderEither` - Implementation - Documentation diff --git a/lib/fpdart.dart b/lib/fpdart.dart index d36f34c8..206bdce3 100644 --- a/lib/fpdart.dart +++ b/lib/fpdart.dart @@ -10,6 +10,7 @@ export 'src/reader.dart'; export 'src/state.dart'; export 'src/task.dart'; export 'src/task_either.dart'; +export 'src/task_option.dart'; export 'src/tuple.dart'; export 'src/typeclass/typeclass.export.dart'; export 'src/typedef.dart'; diff --git a/lib/src/task_option.dart b/lib/src/task_option.dart new file mode 100644 index 00000000..b1d098dd --- /dev/null +++ b/lib/src/task_option.dart @@ -0,0 +1,161 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:fpdart/src/task.dart'; + +/// Tag the [HKT] interface for the actual [TaskOption]. +abstract class _TaskOptionHKT {} + +/// `TaskOption` represents an asynchronous computation that +/// may fails yielding a [None] or returns a `Some(R)` when successful. +/// +/// If you want to represent an asynchronous computation that never fails, see [Task]. +/// +/// If you want to represent an asynchronous computation that returns an object when it fails, +/// see [TaskEither]. +class TaskOption extends HKT<_TaskOptionHKT, R> + with Monad<_TaskOptionHKT, R>, Alt<_TaskOptionHKT, R> { + final Future> Function() _run; + + /// Build a [TaskOption] from a function returning a `Future>`. + const TaskOption(this._run); + + /// Used to chain multiple functions that return a [TaskOption]. + /// + /// You can extract the value of every [Some] in the chain without + /// handling all possible missing cases. + /// If running any of the tasks in the chain returns [None], the result is [None]. + @override + TaskOption flatMap(covariant TaskOption Function(R r) f) => + TaskOption(() => run().then( + (option) async => option.match( + (r) => f(r).run(), + () => Option.none(), + ), + )); + + /// Returns a [TaskOption] that returns `Some(c)`. + @override + TaskOption pure(C c) => TaskOption(() async => Option.of(c)); + + /// Change the return type of this [TaskOption] based on its value of type `R` and the + /// value of type `C` of another [TaskOption]. + @override + TaskOption map2( + covariant TaskOption m1, D Function(R b, C c) f) => + flatMap((b) => m1.map((c) => f(b, c))); + + /// Change the return type of this [TaskOption] based on its value of type `R`, the + /// value of type `C` of a second [TaskOption], and the value of type `D` + /// of a third [TaskOption]. + @override + TaskOption map3(covariant TaskOption m1, + covariant TaskOption m2, E Function(R b, C c, D d) f) => + flatMap((b) => m1.flatMap((c) => m2.map((d) => f(b, c, d)))); + + /// If running this [TaskOption] returns [Some], then return the result of calling `then`. + /// Otherwise return [None]. + @override + TaskOption andThen(covariant TaskOption Function() then) => + flatMap((_) => then()); + + /// If running this [TaskOption] returns [Some], then change its value from type `R` to + /// type `C` using function `f`. + @override + TaskOption map(C Function(R r) f) => ap(pure(f)); + + /// Apply the function contained inside `a` to change the value on the [Some] from + /// type `R` to a value of type `C`. + @override + TaskOption ap(covariant TaskOption a) => + a.flatMap((f) => flatMap((v) => pure(f(v)))); + + /// When this [TaskOption] returns [Some], then return the current [TaskOption]. + /// Otherwise return the result of `orElse`. + /// + /// Used to provide an **alt**ernative [TaskOption] in case the current one returns [None]. + @override + TaskOption alt(covariant TaskOption Function() orElse) => TaskOption( + () async => (await run()).match((_) => run(), () => orElse().run())); + + /// When this [TaskOption] returns a [None] then return the result of `orElse`. + /// Otherwise return this [TaskOption]. + TaskOption orElse(TaskOption Function() orElse) => + TaskOption(() async => (await run()) + .match((r) => TaskOption.some(r).run(), () => orElse().run())); + + /// Convert this [TaskOption] to a [Task]. + /// + /// The task returns a [Some] when [TaskOption] returns [Some]. + /// Otherwise map the type `L` of [TaskOption] to type `R` by calling `orElse`. + Task getOrElse(R Function() orElse) => + Task(() async => (await run()).match(identity, orElse)); + + /// Pattern matching to convert a [TaskOption] to a [Task]. + /// + /// Execute `onNone` when running this [TaskOption] returns a [None]. + /// Otherwise execute `onSome`. + Task match(A Function() onNone, A Function(R r) onSome) => + Task(() async => (await run()).match(onSome, onNone)); + + /// Creates a [TaskOption] that will complete after a time delay specified by a [Duration]. + TaskOption delay(Duration duration) => + TaskOption(() => Future.delayed(duration, run)); + + /// Run the task and return a `Future>`. + Future> run() => _run(); + + /// Build a [TaskOption] that returns a `Some(r)`. + /// + /// Same of `TaskOption.some`. + factory TaskOption.of(R r) => TaskOption(() async => Option.of(r)); + + /// Flat a [TaskOption] contained inside another [TaskOption] to be a single [TaskOption]. + factory TaskOption.flatten(TaskOption> taskOption) => + taskOption.flatMap(identity); + + /// Build a [TaskOption] that returns a `Some(r)`. + /// + /// Same of `TaskOption.of`. + factory TaskOption.some(R r) => TaskOption(() async => Option.of(r)); + + /// Build a [TaskOption] that returns a [None]. + factory TaskOption.none() => TaskOption(() async => Option.none()); + + /// Build a [TaskOption] from the result of running `task`. + factory TaskOption.fromTask(Task task) => + TaskOption(() async => Option.of(await task.run())); + + /// 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) => + TaskOption( + () async => predicate(value) ? Option.of(value) : Option.none()); + + /// Converts a [Future] that may throw to a [Future] that never throws + /// but returns a [Option] instead. + /// + /// Used to handle asynchronous computations that may throw using [Option]. + factory TaskOption.tryCatch(Future Function() run) => + TaskOption(() async { + try { + return Option.of(await run()); + } catch (_) { + return Option.none(); + } + }); + + /// Build a [TaskOption] from `either` that returns [None] when + /// `either` is [Left], otherwise it returns [Some]. + static TaskOption fromEither(Either either) => + TaskOption(() async => either.match((_) => Option.none(), some)); + + /// Converts a [Future] that may throw to a [Future] that never throws + /// but returns a [Option] instead. + /// + /// Used to handle asynchronous computations that may throw using [Option]. + /// + /// It wraps the `TaskOption.tryCatch` factory to make chaining with `flatMap` + /// easier. + static TaskOption Function(A a) tryCatchK( + Future Function(A a) run) => + (a) => TaskOption.tryCatch(() => run(a)); +} diff --git a/test/src/task_option_test.dart b/test/src/task_option_test.dart new file mode 100644 index 00000000..a6d09f29 --- /dev/null +++ b/test/src/task_option_test.dart @@ -0,0 +1,317 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:test/test.dart'; + +void main() { + group('TaskOption', () { + group('tryCatch', () { + test('Success', () async { + final task = TaskOption.tryCatch(() => Future.value(10)); + final r = await task.run(); + r.match((r) => expect(r, 10), () => null); + }); + + test('Failure', () async { + final task = TaskOption.tryCatch(() => Future.error(10)); + final r = await task.run(); + expect(r, isA()); + }); + + test('throws Exception', () async { + final task = TaskOption.tryCatch(() { + throw UnimplementedError(); + }); + final r = await task.run(); + expect(r, isA()); + }); + }); + + group('tryCatchK', () { + test('Success', () async { + final task = TaskOption.of(10); + final ap = task.flatMap(TaskOption.tryCatchK( + (n) => Future.value(n + 5), + )); + final r = await ap.run(); + r.match((r) => expect(r, 15), () => null); + }); + + test('Failure', () async { + final task = TaskOption.of(10); + final ap = task.flatMap(TaskOption.tryCatchK( + (n) => Future.error(n + 5), + )); + final r = await ap.run(); + expect(r, isA()); + }); + + test('throws Exception', () async { + final task = TaskOption.of(10); + final ap = task.flatMap(TaskOption.tryCatchK((_) { + throw UnimplementedError(); + })); + final r = await ap.run(); + expect(r, isA()); + }); + }); + + group('flatMap', () { + test('Some', () async { + final task = TaskOption(() async => Option.of(10)); + final ap = + task.flatMap((r) => TaskOption(() async => Option.of(r + 10))); + final r = await ap.run(); + r.match((r) => expect(r, 20), () => null); + }); + + test('None', () async { + final task = TaskOption(() async => Option.none()); + final ap = + task.flatMap((r) => TaskOption(() async => Option.of(r + 10))); + final r = await ap.run(); + expect(r, isA()); + }); + }); + + group('ap', () { + test('Some', () async { + final task = TaskOption(() async => Option.of(10)); + final ap = task + .ap(TaskOption(() async => Option.of((int c) => c / 2))); + final r = await ap.run(); + r.match((r) => expect(r, 5.0), () => null); + }); + + test('None', () async { + final task = TaskOption(() async => Option.none()); + final ap = task + .ap(TaskOption(() async => Option.of((int c) => c / 2))); + final r = await ap.run(); + expect(r, isA()); + }); + }); + + group('map', () { + test('Some', () async { + final task = TaskOption(() async => Option.of(10)); + final ap = task.map((r) => r / 2); + final r = await ap.run(); + r.match((r) => expect(r, 5.0), () => null); + }); + + test('None', () async { + final task = TaskOption(() async => Option.none()); + final ap = task.map((r) => r / 2); + final r = await ap.run(); + expect(r, isA()); + }); + }); + + group('map2', () { + test('Some', () async { + final task = TaskOption(() async => Option.of(10)); + final ap = task.map2( + TaskOption(() async => Option.of(2)), (b, c) => b / c); + final r = await ap.run(); + r.match((r) => expect(r, 5.0), () => null); + }); + + test('None', () async { + final task = TaskOption(() async => Option.none()); + final ap = task.map2( + TaskOption(() async => Option.of(2)), (b, c) => b / c); + final r = await ap.run(); + expect(r, isA()); + }); + }); + + group('map3', () { + test('Some', () async { + final task = TaskOption(() async => Option.of(10)); + final ap = task.map3( + TaskOption(() async => Option.of(2)), + TaskOption(() async => Option.of(5)), + (b, c, d) => b * c / d); + final r = await ap.run(); + r.match((r) => expect(r, 4.0), () => null); + }); + + test('None', () async { + final task = TaskOption(() async => Option.none()); + final ap = task.map3( + TaskOption(() async => Option.of(2)), + TaskOption(() async => Option.of(5)), + (b, c, d) => b * c / d); + final r = await ap.run(); + expect(r, isA()); + }); + }); + + group('andThen', () { + test('Some', () async { + final task = TaskOption(() async => Option.of(10)); + final ap = + task.andThen(() => TaskOption(() async => Option.of(12.5))); + final r = await ap.run(); + r.match((r) => expect(r, 12.5), () => null); + }); + + test('None', () async { + final task = TaskOption(() async => Option.none()); + final ap = + task.andThen(() => TaskOption(() async => Option.of(12.5))); + final r = await ap.run(); + expect(r, isA()); + }); + }); + + test('pure', () async { + final task = TaskOption(() async => Option.none()); + final ap = task.pure('abc'); + final r = await ap.run(); + r.match((r) => expect(r, 'abc'), () => null); + }); + + test('run', () async { + final task = TaskOption(() async => Option.of(10)); + final future = task.run(); + expect(future, isA()); + final r = await future; + r.match((r) => expect(r, 10), () => null); + }); + + group('fromEither', () { + test('Some', () async { + final task = TaskOption.fromEither(Either.of(10)); + final r = await task.run(); + r.match((r) => expect(r, 10), () => null); + }); + + test('None', () async { + final task = TaskOption.fromEither(Either.left('none')); + final r = await task.run(); + expect(r, isA()); + }); + }); + + group('fromPredicate', () { + test('True', () async { + final task = TaskOption.fromPredicate(20, (n) => n > 10); + final r = await task.run(); + r.match((r) => expect(r, 20), () => null); + }); + + test('False', () async { + final task = TaskOption.fromPredicate(10, (n) => n > 10); + final r = await task.run(); + expect(r, isA()); + }); + }); + + test('fromTask', () async { + final task = TaskOption.fromTask(Task(() async => 10)); + final r = await task.run(); + r.match((r) => expect(r, 10), () => null); + }); + + test('none()', () async { + final task = TaskOption.none(); + final r = await task.run(); + expect(r, isA()); + }); + + test('some()', () async { + final task = TaskOption.some(10); + final r = await task.run(); + r.match((r) => expect(r, 10), () => null); + }); + + group('match', () { + test('Some', () async { + final task = TaskOption(() async => Option.of(10)); + final ex = task.match(() => -1, (r) => r + 10); + final r = await ex.run(); + expect(r, 20); + }); + + test('None', () async { + final task = TaskOption(() async => Option.none()); + final ex = task.match(() => -1, (r) => r + 10); + final r = await ex.run(); + expect(r, -1); + }); + }); + + group('getOrElse', () { + test('Some', () async { + final task = TaskOption(() async => Option.of(10)); + final ex = task.getOrElse(() => -1); + final r = await ex.run(); + expect(r, 10); + }); + + test('None', () async { + final task = TaskOption(() async => Option.none()); + final ex = task.getOrElse(() => -1); + final r = await ex.run(); + expect(r, -1); + }); + }); + + group('orElse', () { + test('Some', () async { + final task = TaskOption(() async => Option.of(10)); + final ex = + task.orElse(() => TaskOption(() async => Option.of(-1))); + final r = await ex.run(); + r.match((r) => expect(r, 10), () => null); + }); + + test('None', () async { + final task = TaskOption(() async => Option.none()); + final ex = + task.orElse(() => TaskOption(() async => Option.of(-1))); + final r = await ex.run(); + r.match((r) => expect(r, -1), () => null); + }); + }); + + group('alt', () { + test('Some', () async { + final task = TaskOption(() async => Option.of(10)); + final ex = task.alt(() => TaskOption(() async => Option.of(20))); + final r = await ex.run(); + r.match((r) => expect(r, 10), () => null); + }); + + test('None', () async { + final task = TaskOption(() async => Option.none()); + final ex = task.alt(() => TaskOption(() async => Option.of(20))); + final r = await ex.run(); + r.match((r) => expect(r, 20), () => null); + }); + }); + + test('of', () async { + final task = TaskOption.of(10); + final r = await task.run(); + r.match((r) => expect(r, 10), () => null); + }); + + test('flatten', () async { + final task = TaskOption>.of(TaskOption.of(10)); + final ap = TaskOption.flatten(task); + final r = await ap.run(); + r.match((r) => expect(r, 10), () => null); + }); + + test('delay', () async { + final task = TaskOption(() async => Option.of(10)); + final ap = task.delay(const Duration(seconds: 2)); + final stopwatch = Stopwatch(); + stopwatch.start(); + await ap.run(); + stopwatch.stop(); + expect(stopwatch.elapsedMilliseconds >= 2000, true); + }); + }); +}