Open Meteo API - Functional programming with fpdart (Part 1)

Sandro Maglione

Sandro Maglione

Functional programming

In this series we are going to learn how to convert the Open Meteo API example (from the bloc package) from imperative code to functional programming using fpdart.

We will see step-by-step how to refactor the code to functional programming, as well as the benefits that this brings.

Each post of the series will cover in details one specific aspect of the refactoring.


In this first post:

  • Introduction to the Open Meteo API code example
  • Refactoring of the return type of the function from Future to TaskEither
    • What is the difference between Future and Task
    • How TaskEither works (Task async + Either error handling)
    • How to encode errors using Either and abstract class

Open Meteo API example

We are going to refactor the Open Meteo API client using fpdart and functional programming.

This example is part of the flutter_weather app example in the bloc package.

The implementation is relatively simple. Nonetheless, it contains some of the most usual usecases of a real production app, like:

  • Making an http request (using the http package)
  • Checking for a valid response
  • Decoding the response from JSON
  • Validating the response format

We are going to focus specifically on the locationSearch request, that you can see reported below 👇:

open_meteo_api_client.dart
/// Finds a [Location] `/v1/search/?name=(query)`.
Future<Location> locationSearch(String query) async {
  final locationRequest = Uri.https(
    _baseUrlGeocoding,
    '/v1/search',
    {'name': query, 'count': '1'},
  );
 
  final locationResponse = await _httpClient.get(locationRequest);
 
  if (locationResponse.statusCode != 200) {
    throw LocationRequestFailure();
  }
 
  final locationJson = jsonDecode(locationResponse.body) as Map;
 
  if (!locationJson.containsKey('results')) throw LocationNotFoundFailure();
 
  final results = locationJson['results'] as List;
 
  if (results.isEmpty) throw LocationNotFoundFailure();
 
  return Location.fromJson(results.first as Map<String, dynamic>);
}

The goal is to see step by step what you must consider to convert this implementation to functional programming using fpdart.

Furthermore, we are going to highlight the reason behind each refactoring, showing the potential benefits along the way.


Future is not your friend

Let's start from the very top: the return type, Future<Location>. Here is a general rule that you must follow when working with fpdart and functional programming:

Do not return Future from a function, use Task, TaskOption, or TaskEither instead

You can read my previous post about sync and async functions, and why Future is not functional-friendly.

The main reason is that Future makes the function impure: the result of calling the function will change at every request. Furthermore, Future is not composable and it makes error handling complex and verbose.

I strongly suggest you to read this post about Future & Task: asynchronous Functional Programming, which goes into more details on the difference between Future and Task

fpdart has 3 alternatives, each suited for different situations based on the requirements:

  • Task: Used to perform async requests that you are 100% sure that will never fail (100% ☝️)
  • TaskOption: Used to perform async requests that may fail and/or return a missing value
  • TaskEither: Used to perform async requests that may fail for multiple reasons that we want to encode in the return type

What makes Task functional programming friendly?

You may be thinking: "Wait, but making an http request will always return a different result on every call of the function, how can Task make the function pure?".

Simple: Task doesn't make the request, yet. In fact, Task is implemented as follows 👇:

task.dart
class Task<A> {
  final Future<A> Function() _run;
 
  /// Build a [Task] from a function returning a [Future].
  const Task(this._run);
 
  /// Run the task and return a [Future].
  Future<A> run() => _run();

Task is a thunk: a function that, when called, will return a Future.

Until you call the run() method, not request is executed.

Task is simply a description of your request, it doesn't do anything until run() is called 💁🏼‍♂️

That's what makes the function pure and composable: you can now chain methods to extract and validate the response, without actually making any request 🪄.

Again, you can learn more about Task in Future & Task: asynchronous Functional Programming

Which Task should I choose?

Task, TaskOption, or TaskEither?

As a rule of thumb, TaskEither is generally what you are looking for.

TaskEither allows to encode the error directly in the response type. It takes 2 generic parameters (TaskEither<L, R>):

  • L (Left): represents the error type in case the request fails
  • R (Right): represent the response type in case of a successful request

We are going to use TaskEither in this example. What types should we give it? 🤔

Error handling with the Either type

In functional programming, the response type of the function should give us all the information needed to handle all possible events inside the app.

TaskEither (which is a fusion of Task + Either) allows to do just that:

  • Since it's a Task, we know we are making an async request
  • Since it's an Either, we know the request can fail

Furthermore, Either<L, R> will give us even more information:

  • If the function fails, all the possible failures are described by the L (Left) type
  • If the function succeeds, the response type is R (Right)

Just by reading the return type, we know everything that we need to handle any possible response in the app (regardless of how the function is implemented internally)

How to encode any possible error using Either

The function can fail for multiple reasons, but we have only 1 error type allowed.

That's the perfect usecase for an abstract class.

We define an abstract class which represents our error:

location_failure.dart
/// Abstract class which represents a failure in the `locationSearch` request.
abstract class OpenMeteoApiFpdartLocationFailure {}

Now all the possible errors in the response will implement OpenMeteoApiFpdartLocationFailure, for example:

/// [OpenMeteoApiFpdartLocationFailure] when location response
/// cannot be decoded from json.
class LocationInvalidJsonDecodeFpdartFailure
    implements OpenMeteoApiFpdartLocationFailure {
  const LocationInvalidJsonDecodeFpdartFailure(this.body);
  final String body;
}

Finally, we can define the response type of the locationSearch function:

/// Finds a [Location] `/v1/search/?name=(query)`.
TaskEither<OpenMeteoApiFpdartLocationFailure, Location> locationSearch(String query)

Notice how the function is no more async, since it does not return a Future

To recap:

  • TaskEither: async request that can fail
  • OpenMeteoApiFpdartLocationFailure: class which encodes all the possible errors
  • Location: result of a successful response

That is all for this first part.

In the second part of this series we are going to learn how to make http requests in functional programming, and how to validate the response.

You can subscribe to the newsletter here below to stay up to date with each new post 👇

👋・Interested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.