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

Sandro Maglione

Sandro Maglione

Mobile 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:

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

Subscribe to the newsletter

In my newsletter I share ideas, articles, tips and tutorials about Functional Programming and Frontend Development with Dart & Typescript (๐Ÿ“ฑ + ๐Ÿ–ฅ๏ธ)

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);
}
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);
}
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() { ... }
}
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();
/// 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);
/// This function has [Dependency1] and [Dependency2] as "dependencies"
int someMethod(Dependency1 dep1, Dependency2 dep2) { ... }
 
someMethod(dep1, dep2);

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) {}
Either<SomeError, SuccessValue> someMethod(Dependency1 dep1, Dependency2 dep2) {}

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

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();
}
/// `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) {}
Either<SomeError, SuccessValue> someMethod(Dependency1 dep1, Dependency2 dep2) {}

Subscribe to the newsletter

In my newsletter I share ideas, articles, tips and tutorials about Functional Programming and Frontend Development with Dart & Typescript (๐Ÿ“ฑ + ๐Ÿ–ฅ๏ธ)

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 = ...
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 = ...
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();
}
/// 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 */);
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.