Skip to content

Commit

Permalink
Do notation (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
SandroMaglione authored May 5, 2023
2 parents 38436a0 + 2eabe0d commit 55630dc
Show file tree
Hide file tree
Showing 32 changed files with 1,013 additions and 89 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# v0.6.0-dev.1 - 8 March 2023
- Do notation [#97](https://github.com/SandroMaglione/fpdart/pull/97) 🎉

# v0.5.0 - 4 March 2023
- Updates to `Option` type [#92](https://github.com/SandroMaglione/fpdart/pull/92) [⚠️ **BREAKING CHANGE**]
- Added `const factory` constructor for `None` (fixes [#95](https://github.com/SandroMaglione/fpdart/issues/95))
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Check out also this series of articles about functional programming with `fpdart
```yaml
# pubspec.yaml
dependencies:
fpdart: ^0.5.0 # Check out the latest version
fpdart: ^0.6.0-dev.1 # Check out the latest version
```
## ✨ Examples
Expand Down
83 changes: 83 additions & 0 deletions example/do_notation/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:fpdart/fpdart.dart';

TaskEither<String, String> getUsernameFromId(int id) => TaskEither.of('sandro');
TaskEither<String, String> getProfilePicture(String username) =>
TaskEither.of('image');
int getPictureWidth(String image) => 10;
TaskEither<String, bool> updatePictureWidth(int width) => TaskEither.of(true);

Future<String> getUsernameFromIdLinear(int id) async => 'sandro';
Future<String> getProfilePictureLinear(String username) async => 'image';
int getPictureWidthLinear(String image) => 10;
Future<bool> updatePictureWidthLinear(int width) async => true;

/// Linear (no fpdart)
Future<bool> changePictureSizeFromIdLinear(int id) async {
final username = await getUsernameFromIdLinear(id);
final image = await getProfilePictureLinear(username);
final width = getPictureWidthLinear(image);
return updatePictureWidthLinear(width);
}

/// Chaining
TaskEither<String, bool> changePictureSizeFromId(int id) =>
getUsernameFromId(id)
.flatMap((username) => getProfilePicture(username))
.map((image) => getPictureWidth(image))
.flatMap((width) => updatePictureWidth(width));

/// Do notation
TaskEither<String, bool> changePictureSizeFromIdDo(int id) =>
TaskEither<String, bool>.Do(
($) async {
final username = await $(getUsernameFromId(id));
final image = await $(getProfilePicture(username));
final width = getPictureWidth(image);
return $(updatePictureWidth(width));
},
);

/// [map]: Update value inside [Option]
Option<int> map() => Option.of(10)
.map(
(a) => a + 1,
)
.map(
(b) => b * 3,
)
.map(
(c) => c - 4,
);

Option<int> mapDo() => Option.Do(($) {
final a = $(Option.of(10));
final b = a + 1;
final c = b * 3;
return c - 4;
});

/// [flatMap]: Chain [Option]
Option<int> flatMap() => Option.of(10)
.flatMap(
(a) => Option.of(a + 1),
)
.flatMap(
(b) => Option.of(b * 3),
)
.flatMap(
(c) => Option.of(c - 4),
);

Option<int> flatMapDo() => Option.Do(($) {
final a = $(Option.of(10));
final b = $(Option.of(a + 1));
final c = $(Option.of(b * 3));
return $(Option.of(c - 4));
});

/// [andThen]: Chain [Option] without storing its value
Option<int> andThen() => Option.of(10).andThen(() => Option.of(20));
Option<int> andThenDo() => Option.Do(($) {
$(Option.of(10)); // Chain Option, but do not store the result
return 20;
});
2 changes: 1 addition & 1 deletion example/json_serializable/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ packages:
path: "../.."
relative: true
source: path
version: "0.5.0"
version: "0.6.0-dev.1"
frontend_server_client:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion example/managing_imports/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ packages:
path: "../.."
relative: true
source: path
version: "0.5.0"
version: "0.6.0-dev.1"
frontend_server_client:
dependency: transitive
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,46 +28,52 @@ class OpenMeteoApiClientFpdart {
),
),
LocationHttpRequestFpdartFailure.new,
)
.chainEither(
(response) =>
_validResponseBody(response, LocationRequestFpdartFailure.new),
)
.chainEither(
(body) => Either.tryCatch(
).chainEither(
(response) => Either.Do(($) {
final body = $(
_validResponseBody(response, LocationRequestFpdartFailure.new),
);

final json = $(
Either.tryCatch(
() => jsonDecode(body),
(_, __) => LocationInvalidJsonDecodeFpdartFailure(body),
),
)
.chainEither(
(json) => Either<OpenMeteoApiFpdartLocationFailure,
);

final data = $(
Either<OpenMeteoApiFpdartLocationFailure,
Map<dynamic, dynamic>>.safeCast(
json,
LocationInvalidMapFpdartFailure.new,
),
)
.chainEither(
(body) => body
);

final currentWeather = $(
data
.lookup('results')
.toEither(LocationKeyNotFoundFpdartFailure.new),
)
.chainEither(
(currentWeather) => Either<OpenMeteoApiFpdartLocationFailure,
List<dynamic>>.safeCast(
);

final results = $(
Either<OpenMeteoApiFpdartLocationFailure, List<dynamic>>.safeCast(
currentWeather,
LocationInvalidListFpdartFailure.new,
),
)
.chainEither(
(results) =>
results.head.toEither(LocationDataNotFoundFpdartFailure.new),
)
.chainEither(
(weather) => Either.tryCatch(
);

final weather = $(
results.head.toEither(LocationDataNotFoundFpdartFailure.new),
);

return $(
Either.tryCatch(
() => Location.fromJson(weather as Map<String, dynamic>),
LocationFormattingFpdartFailure.new,
),
);
}),
);

/// Fetches [Weather] for a given [latitude] and [longitude].
TaskEither<OpenMeteoApiFpdartWeatherFailure, Weather> getWeather({
Expand Down
2 changes: 1 addition & 1 deletion example/open_meteo_api/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ packages:
path: "../.."
relative: true
source: path
version: "0.5.0"
version: "0.6.0-dev.1"
frontend_server_client:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion example/pokeapi_functional/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ A new Flutter project.
## Getting Started

- `flutter pub get`
- `flutter pub run build_runner build`
- `dart run build_runner build`
- `flutter run -d chrome`
20 changes: 14 additions & 6 deletions example/pokeapi_functional/lib/api/fetch_pokemon.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:convert';

import 'package:http/http.dart' as http;
import 'package:fpdart/fpdart.dart';
import 'package:http/http.dart' as http;
import 'package:pokeapi_functional/constants/constants.dart';
import 'package:pokeapi_functional/models/pokemon.dart';

Expand Down Expand Up @@ -51,9 +51,17 @@ TaskEither<String, Pokemon> fetchPokemon(int pokemonId) => TaskEither.tryCatch(
///
/// All the functions are simply chained together following the principle of composability.
TaskEither<String, Pokemon> fetchPokemonFromUserInput(String pokemonId) =>
_validateUserPokemonId(pokemonId).flatMapTask(fetchPokemon);
TaskEither.Do(($) async {
final validPokemonId = await $(_validateUserPokemonId(
pokemonId,
).toTaskEither());
return $(fetchPokemon(validPokemonId));
});

TaskEither<String, Pokemon> fetchRandomPokemon() => randomInt(
Constants.minimumPokemonId,
Constants.maximumPokemonId + 1,
).toIOEither<String>().flatMapTask(fetchPokemon);
TaskEither<String, Pokemon> fetchRandomPokemon = TaskEither.Do(($) async {
final pokemonId = await $(randomInt(
Constants.minimumPokemonId,
Constants.maximumPokemonId + 1,
).toTaskEither());
return $(fetchPokemon(pokemonId));
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ part 'pokemon_provider.g.dart';
class PokemonState extends _$PokemonState {
@override
FutureOr<Pokemon> build() async =>
fetchRandomPokemon().getOrElse((l) => throw Exception(l)).run();
fetchRandomPokemon.getOrElse((l) => throw Exception(l)).run();

/// User request, try to convert user input to [int] and then
/// request the pokemon if successful.
Expand Down
14 changes: 7 additions & 7 deletions example/pokeapi_functional/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ packages:
path: "../.."
relative: true
source: path
version: "0.5.0"
version: "0.6.0-dev.1"
freezed:
dependency: "direct main"
description:
Expand Down Expand Up @@ -390,10 +390,10 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
sha256: "586678f20e112219ed0f73215f01bcdf1d769824ba2ebae45ad918a9bfde9bdb"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.3.0"
meta:
dependency: transitive
description:
Expand Down Expand Up @@ -531,10 +531,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
version: "1.10.0"
stack_trace:
dependency: transitive
description:
Expand Down Expand Up @@ -587,10 +587,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
sha256: daadc9baabec998b062c9091525aa95786508b1c48e9c30f1f891b8bf6ff2e64
url: "https://pub.dev"
source: hosted
version: "0.5.1"
version: "0.5.2"
timing:
dependency: transitive
description:
Expand Down
65 changes: 39 additions & 26 deletions example/read_write_file/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,51 @@ class FoundWord {
/// Word to search in each sentence
const searchWords = ['that', 'and', 'for'];

Iterable<FoundWord> collectFoundWords(
Iterable<Tuple2<String, String>> iterable,
) =>
iterable.flatMapWithIndex(
(tuple, index) => searchWords.foldLeftWithIndex<List<FoundWord>>(
[],
(acc, word, wordIndex) =>
tuple.second.toLowerCase().split(' ').contains(word)
? [
...acc,
FoundWord(
index,
word,
wordIndex,
tuple.second.replaceAll(word, '<\$>'),
tuple.first,
),
]
: acc,
),
);

void main() async {
/// Read file async using [TaskEither]
///
/// Since we are using [TaskEither], until we call the `run` method,
/// no actual reading is performed.
final task = readFileAsync('./assets/source_ita.txt')
final collectDoNotation = TaskEither<String, Iterable<FoundWord>>.Do(
($) async {
final linesIta = await $(readFileAsync('./assets/source_ita.txt'));
final linesEng = await $(readFileAsync('./assets/source_eng.txt'));
final linesZip = linesIta.zip(linesEng);
return collectFoundWords(linesZip);
},
);

final collectFlatMap = readFileAsync('./assets/source_ita.txt')
.flatMap(
(linesIta) => readFileAsync('./assets/source_eng.txt').map(
(linesEng) => linesIta.zip(linesEng),
),
)
.map(
(iterable) => iterable.flatMapWithIndex(
(tuple, index) => searchWords.foldLeftWithIndex<List<FoundWord>>(
[],
(acc, word, wordIndex) =>
tuple.second.toLowerCase().split(' ').contains(word)
? [
...acc,
FoundWord(
index,
word,
wordIndex,
tuple.second.replaceAll(word, '<\$>'),
tuple.first,
),
]
: acc,
),
),
)
.match(
.map(collectFoundWords);

/// Read file async using [TaskEither]
///
/// Since we are using [TaskEither], until we call the `run` method,
/// no actual reading is performed.
final task = collectDoNotation.match(
(l) => print(l),
(list) {
/// Print all the found [FoundWord]
Expand Down
2 changes: 1 addition & 1 deletion example/read_write_file/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ packages:
path: "../.."
relative: true
source: path
version: "0.5.0"
version: "0.6.0-dev.1"
lint:
dependency: "direct dev"
description:
Expand Down
Loading

0 comments on commit 55630dc

Please sign in to comment.