โ€ข

tech

Dependencies and Errors in Functional Programming | Fpdart and Riverpod Functional Programming in Flutter

Learn how to define and use dependencies and errors in functional programming with fpdart: dependency injection, Either type, records, pattern matching, and using ReaderTaskEither to put all together.


Sandro Maglione

Sandro Maglione

Software development

This is the third part of a new series in which we are going to learn how to build a safe, maintainable, and testable app in Flutter using fpdart and riverpod.

We will focus less on the implementation details, and more on good practices and abstractions that will helps us to build a flexible yet resilient app in Flutter.

As always, you can find the final Open Source project on Github:

fpdart_riverpod

In this article we are going to start working with fpdart to implement the requests to read and write in storage:

  • How to handle dependencies in functional programming (dependency injection)
  • How to handle errors in functional programming (Either)
  • How to use the ReaderTaskEither type in fpdart

There is more ๐Ÿคฉ

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.

Recap: EventEntity and StorageService

In the previous article we defined the requirements for our application as follows:

The app should allow to store events (with title and date). It then should allow to visualize all the created events.

We then implemented two important building blocks for the app: EventEntity and StorageService.

EventEntity is an immutable class that stores an event:

event_entity.dart
import 'package:flutter/material.dart';

@immutable
final class EventEntity {
  final int id;
  final String title;
  final DateTime createdAt;

  const EventEntity(this.id, this.title, this.createdAt);
}

StorageService is an abstract class that defines the API methods to read and write data in storage:

storage_service.dart
abstract class StorageService {
  Future<List<EventEntity>> get getAll;
  Future<EventEntity> put(String title);
}

We are now going to use both EventEntity and StorageService to define the actual storage implementation using fpdart.


Functional programming function definition: fpdart overview

Before working on the actual code using fpdart it's important to clearly define the requirements.

Specifically, we want to explicitly define 3 types:

  • Dependencies
  • Errors
  • Success

Dependencies

A dependency is any extra service or context required to perform the request.

In object oriented programming, a dependency is usually defined as a parameter in a class, which is provided when initializing a concrete instance (dependency injection):

class GetAllRequest {
  final Dependency1 dep1;
  final Dependency2 dep1;

  const GetAllRequest(this.dep1, this.dep2);

  int someMethod() { ... }
}

Using this class-based approach, you are required to provide each dependency before using GetAllRequest

/// Create and provide `dep1` and `dep2` before defining `getAllRequest`
final getAllRequest = GetAllRequest(dep1, dep2);

getAllRequest.someMethod();

In fpdart (and functional programming in general) it works the opposite way: first define the function, and then provide the dependencies.

Note ๐Ÿ’ก: This is the difference between working with functions instead of classes, you first define the function and then (only at the very end โ˜๏ธ) provide the parameters

/// This function has [Dependency1] and [Dependency2] as "dependencies"
int someMethod(Dependency1 dep1, Dependency2 dep2) { ... }

someMethod(dep1, dep2);

Dependency injection for testing โœ… It works, but it's not nice to define, use, and compose ๐Ÿ˜• There is a solution, it comes from `fpdart`, and it's called `Reader` ๐Ÿ‘‡๐Ÿงต

Image
Sandro Maglione
Sandro Maglione
@SandroMaglione

How to solve this? Make it explicit โ˜๏ธ Provide "print" as a dependency (parameter) to the function

Image
15
Reply

Important โ˜๏ธ: All dependencies must be explicit when working with functional programming and fpdart. Making dependencies explicit makes the code easier to test and read.

This means that ideally you are not allowed to access global functions ๐Ÿ™…

In our case we want to use StorageService to make a getAll request. Therefore, StorageService is a dependency, required to perform the request.

Note ๐Ÿ’ก: A dependency in fpdart is always defined as an abstract class.

In our example, fpdart only knows about the getAll method, without any specific implementation details.

Errors

In functional programming we prefer to avoid throwing Exceptions and using try/catch. Instead, each error is explicitly defined in the return type of a function.

In fpdart we use the Either type to encode both error and success values when defining the return type of every function:

Either<SomeError, SuccessValue> someMethod(Dependency1 dep1, Dependency2 dep2) {}

There is another way to handle errors ๐Ÿ”ฅ Which doesn't use try/catch/throw at all ๐Ÿ™…โ€โ™‚๏ธ It's called *Either* This is a big deal, let's learn how this works ๐Ÿ‘‡๐Ÿงต x.com/SandroMaglioneโ€ฆ

Sandro Maglione
Sandro Maglione
@SandroMaglione

Error handling is crucial for any app โš› It can make or break your app (literally ๐Ÿ’๐Ÿผโ€โ™‚๏ธ) #dart offers many tools to save you from troubles ๐ŸŽฏ Here they are ๐Ÿ‘‡๐Ÿงต

12
Reply

In this example, SomeError encodes every possible error that may occur when calling the function (recoverable errors ๐Ÿ‘‡).

Recoverable VS Unrecoverable errors ๐Ÿค” Here is a good analogy ๐Ÿ˜‚๐Ÿ‘‡ "Nurse, if there's a patient in room 5, can you ask him to wait?" โ˜‘๏ธ Recoverable error: "Doctor, there is no patient in room 5." โ›”๏ธUnrecoverable error: "Doctor, there is no room 5!"

Sandro Maglione
Sandro Maglione
@SandroMaglione

What if your #flutter app fails? Should you throw? Error? Exception? #dart gives you both Error and Exception ๐ŸŽฏ But how do they work? Which one should you choose? ๐Ÿค” Here is the answer ๐Ÿ‘‡๐Ÿงต

3
Reply

Since Dart 3 defining type-safe errors has become much easier using sealed classes:

/// `RequestError` is the type using in [Either].
///
/// We then use pattern matching to handle all possible errors (`sealed` class ๐Ÿค)
sealed class RequestError {
  const RequestError();
}

class RequestError1 extends RequestError {
  const RequestError1();
}

class RequestError2 extends RequestError {
  const RequestError2();
}

Success value

The last type to define is the success value. This value is returned when the request is successful.

The success value is the same type that you use as return type in normal dart code (without Either).

The success type is defined in the "right" side of the Either type:

Either<SomeError, SuccessValue> someMethod(Dependency1 dep1, Dependency2 dep2) {}

There is more ๐Ÿคฉ

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.

ReaderTaskEither: Dependencies + Errors + Success

We are now going to put all these types together using fpdart ๐Ÿ”ฅ

Since fpdart v1.0 you can use the full power of the ReaderTaskEither type.

ReaderTaskEither encodes a function that:

  1. Requires some dependencies (Reader)
  2. May fail with some error (Either)
  3. Returns an async success value (Task)

All these parameters are explicitly defined in the return type:

ReaderTaskEither<Dependency, Error, Success> someMethod = ...

Multiple dependencies using Records

Since Dart 3 we can use Records to organize multiple dependencies in one type:

typedef Dependencies = ({ Dependency1 dep1, Dependency2 dep2 });

ReaderTaskEither<Dependencies, Error, Success> someMethod = ...

Multiple errors using sealed classes

As we saw in the previous section, we can use sealed and pattern matching to define every possible error:

/// Use pattern matching to handle all possible errors (`sealed` class ๐Ÿค)
sealed class RequestError {
  const RequestError();
}

class RequestError1 extends RequestError {
  const RequestError1();
}

class RequestError2 extends RequestError {
  const RequestError2();
}

Putting all together using ReaderTaskEither

Defining dependencies, errors, and success value is a pre-requisite to implement a type-safe method using fpdart:

ReaderTaskEither<Dependencies, RequestError, Success> getAllEvents = ReaderTaskEither(/* TODO */);

The next step is the actual implementation inside ReaderTaskEither ๐Ÿค


This is it for part 3!

As you saw we did not yet write any actual code ๐Ÿ’๐Ÿผโ€โ™‚๏ธ. Instead, we spent more time making every aspect of the app explicit (dependencies, errors, and return value).

This setup will make the actual implementation easier and safer: we know exactly all the methods at our disposal, and we know exactly all the possible errors. fpdart will then guide using the type-system to avoid making implementation errors (using ReaderTaskEither) ๐Ÿช„

If you want to stay up to date with the latest releases, you can subscribe to my newsletter here below ๐Ÿ‘‡

Thanks for reading.

๐Ÿ‘‹ใƒปInterested in learning more, every week?

Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.