Fpdart, Functional Programming in Dart and Flutter

24 July 2022οΌ»updatedοΌ½

β€’

7 min read

β€’ Functional programming

Fpdart is a package for Functional Programming in Dart and Flutter. The goal of fpdart is to enable every dart developer to learn and use Functional Programming in his applications.

Functional Programming is easy, if you know where to start. That's why fpdart provides a complete documentation for every method and class. Even if you know nothing about Functional Programming, you can start using the package today.

In this article, I am going to convince you to try Functional Programming in Dart. I will explain step by step how to start using fpdart and why it will help you write better code, all with the power of Functional Programming.

This introduction does not assume any previous knowledge about Functional Programming. Just keep reading and you will easily understand all you need to know to power up your applications.


Why should I care about Functional Programming

Forget all you heard about Functional Programming, let's start from scratch. No math, algebra, monads. The only thing everybody wants to know is: "How can I write better code faster?".

When you start your journey as a developer, your primary goal is to make something that works. No need for any framework, library, state management solution. However, as you gain more experience, eventually you start importing more and more packages. Why would you do that?

Every package is a solution to reduce complexity. You accept a black-box that some other developer wrote for you and you just interact with a simpler API. The underlying principle that allows you to use external solutions is called composability.

When do things start to break? When the application loses composability. Less composability means more code, more code means more bugs and complexity. More complexity means a nightmare for you as a developer. Let us see a simple example.

Imperative VS Functional code

/// Sum elements of a list
const List<int> list = [1, 2, 3, 4];

/// Imperative solution
int sumI = 0;
for (int i = 0; i < list.length; ++i) {
  sumI = sumI + list[i];
}

/// Functional solution
final sumF = list.fold<int>(0, (p, c) => p + c);

Look at the difference between this two solutions, Imperative and Functional.

Imperative means telling step by step the machine what to do. It involves more variables, more code, more complexity. All of this for a simple sum.

Functional means telling only what you care about, and let the machine doing its work. Using the fold method, you declare where to start (0) and what to do (sum). No more, no less. That is the core of Functional Programming.

Functional Programming is all about reducing complexity. Specifying only the parameters that are relevant to your application, and nothing else.

Imperative code is not composable!

Now, let's extend the previous example. We now want only the sum of the numbers greater than 2. How do we achieve that?

/// Composability
/// Sum all elements of a list that are greater than 2
/// Imperative solution
int sumI = 0;
for (int i = 0; i < list.length; ++i) {
  final value = list[i];
  if (value > 2) {
    sumI = sumI + value;
  }
}

/// Functional solution
final sumF = list.where((e) => e > 2).fold<int>(0, (p, c) => p + c);

If you want to extend the previous example in the Imperative case, you must first understand all the previous code. You cannot compose your solution with the previous one. You must go inside the for loop and add another piece of logic.

In Functional Programming, you add a step before the sum to select only numbers greater than two. Everything else is the same. This is what composability is all about!

The problem with Imperative code

As we have seen in the example above, Imperative coding is about adding more and more logic and lines of code to an existing codebase. This introduces some problems:

  • Growing complexity.
  • Code becomes less and less readable.
  • It is more difficult to extract functionalities in separated functions. This creates a lot of duplicated code.
  • Worst developer experience for you and everyone that will read your code.

Let us see an extreme example. Can you achieve the same result of the code below using imperative programming?

/// Extreme example
///
/// How can you achieve the same result with Imperative code?
/// Is it even possible? πŸ€·β€
final result = list
    .where((e) => e > 2)
    .plus([1, 2, 3])
    .drop(2)
    .intersect([1, 2, 3])
    .map((e) => e * 2)
    .take(3)
    .first;

The above example shows the power of Functional Programming and composability at their peak!

Since every function does not change the original list but returns another list, it becomes possible to chain multiple functions together. Every function only knows about itself. It does its job and returns a result, simple!

Fpdart, Functional Programming in Dart

Fpdart brings Functional Programming to Dart and Flutter. It provides all the main Functional Programming types that many other Functional languages provide out of the box.

# pubspec.yaml
dependencies:
  fpdart: ^0.0.5 # Check out the latest version

In future articles we are going to learn step by step how to use each single type provided by fpdart.

For now, let's see how you can start using fpdart in your code.

Option (Some, None)

Use Option to handle missing values. Instead of using null, you define the type to be Option:

/// [Option]
const int? a = null;
final Option<int> b = none<int>();

/// You must manually handle missing values
int resultI = 0;
if (a != null) {
  resultI = a * 2;
}

/// No need to check for `null`
final resultF = b.getOrElse(() => 0) * 2;

This is useful when a function cannot return a value in some edge cases. Instead of throwing an exception (do not do that!), use Option:

/// Don't do that! ⚠
double divideI(int x, int y) {
  if (y == 0) {
    throw Exception('Cannot divide by 0!');
  }

  return x / y;
}

/// Error handling using [Option] πŸŽ‰
Option<double> divideF(int x, int y) {
  if (y == 0) {
    return none();
  }

  return some(x / y);
}

Check out my article about how to use Maybe/Option in your code.

Either (Right, Left)

Used to handle errors. Either can be Right or Left (no both!). Right contains the value returned when the function is successful, Left contains some error or message when the function fails (Did I say already to not throw exceptions?):

/// Don't do that! ⚠
double divideI(int x, int y) {
  if (y == 0) {
    throw Exception('Cannot divide by 0!');
  }

  return x / y;
}

/// Error handling using [Either] πŸŽ‰
Either<String, double> divideF(int x, int y) {
  if (y == 0) {
    return left('Cannot divide by 0');
  }

  return right(x / y);
}

Task

Task allows you to run asyncronous functions in a more composable way:

/// You must run one [Future] after the other, no way around this...
Future<int> asyncI() {
  return Future.value(10).then((value) => value * 10);
}

/// No need of `async`, you decide when to run the [Future] ⚑
Task<int> asyncF() {
  return Task(() async => 10).map((a) => a * 10);
}

TaskEither

What if a Future (Task) may fail? Let's use TaskEither to handle errors:

/// What error is that? What is [dynamic]?
Future<int> asyncI() {
  return Future<int>.error('Some error!')
      .then((value) => value * 10)
      .catchError((dynamic error) => print(error));
}

/// Handle all the errors easily ✨
TaskEither<String, int> asyncF() {
  return TaskEither<String, int>(
    () async => left('Some error'),
  ).map((r) => r * 10);
}

This article was just an introduction to what fpdart and the world of Functional Programming in dart can offer.

There is a lot more to explore. In future articles, we will dive deeper into every class to understand how it works and why you would want to use it.

If you are interested and you want to know more, sign up for my newsletter here below and follow me on Twitter at @SandroMaglione.