Getting started with fpdart and Functional programming in dart

Sandro Maglione

Sandro Maglione

Mobile development

fpdart v1 is out! After months of development, community discussions, and testings, the newest major version of fpdart is ready for general use.

This post is an overview of everything that fpdart has to offer:

  • Overview of all the types, why they exist, and how to use them
  • How fpdart manages side effects and immutability for coding using functional programming
  • Overview of all the extension methods and utility functions provided by the package

This is your definitive getting started guide for fpdart, get ready πŸ‘‡


What is fpdart

fpdart is a package that brings functional programming to dart.

fpdart provides functional programming types and methods aimed at making easier to use functional programming in any dart and flutter application.

fpdart was released in 2021. After two years of development, fpdart v1.0.0 has been released this week.

When should I use fpdart

fpdart is ideal to implement the business logic of your application. The package takes advantage of the dart's type system to reduce errors by using types.

The 2 core principles of functional programming are:

  • Pure functions
  • Immutability

fpdart is built on top of these principles to make your codebase easier to implement, maintain and test.


Getting started with fpdart

fpdart is available on pub.dev. All you need to do to start using the package is to add it to your dependencies in pubspec.yaml:

pubspec.yaml
dependencies:
  fpdart: ^1.0.0

fpdart is a pure dart package with has zero dependencies 🀝

It runs all all platforms supported by dart and flutter (mobile, web, and desktop)

In this post we are going to explore the full API of fpdart.

fpdart provides many different types, each designed to handle a specific usecase.

For each of these types, I am going to first show a native dart code example, explain some issues and potential errors, and then how to refactor the code to take advantage of fpdart's types.

The fpdart API is inspired by other functional programming languages and libraries: Haskell, fp-ts, Scala.

I suggest you to read some resources about some of these technologies if you ever get stuck or if you want to learn more

There is more.

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 700+ readers.

Error handling: Option and Either

We have been asked to implement a function that returns the 3rd letter of a given String. This is rather easy πŸ’πŸΌβ€β™‚οΈ:

String getThirdLetter(String str) => str[2];

Well, something is wrong here. The app crashes when running the following example:

/// There is no "3rd" letter
getThirdLetter("ab");
Unhandled exception:
RangeError (index): Invalid value: Not in inclusive range 0..1: 2

This may look trivial on the surface, but in reality these kind of errors can become quite tricky to spot and debug.

What is the issue really? Answer: the return type.

The function signature is lying. String is not the correct return type.

Functions that do not return a value for some inputs are called Partial Functions

A possible solution is to introduce null:

String? getThirdLetterOrNull(String str) => str.length >= 3 ? str[2] : null;

This solution is not ideal.

First of all, having to handle the null case may become verbose, especially when we need to compose functions together:

String? getThirdLetterOrNull(String str) => str.length >= 3 ? str[2] : null;
 
int? searchLetterInString(String str, String letter) {
  int index = str.indexOf(letter);
  return index == -1 ? null : index;
}
 
 
void main(List<String> args) {
  final thirdLetter = getThirdLetterOrNull("ab");
  if (thirdLetter != null) {
    final letterIndex = searchLetterInString("abcde", thirdLetter);
 
    if (letterIndex != null) {
      /// ...
    }
  }
}

The second issue is error handling:

int program(String source, String search) {
  final thirdLetter = getThirdLetterOrNull(source);
  if (thirdLetter != null) {
    final letterIndex = searchLetterInString(search, thirdLetter);
    if (letterIndex != null) {
      return letterIndex;
    } else {
      throw Exception("Letter not found");
    }
  } else {
    throw Exception("No third letter");
  }
}

The first option would be returning null again, which may cause problems like before.

Otherwise, as shown in the example code, the usual dart pattern is to throw when some value is invalid.

Using throw allows to return int instead of int?. Nonetheless, if we forget to handle the error case (catch), or our API user simply does not know that program may fail, the app will crash.

It is not documented anywhere that program may throw. The only solution is reading directly the source code.

Reading the source code directly becomes problematic when we are working with abstract classes, since an IDE usually redirects you to the original abstract class instead of the concrete implementation.

Furthermore, a function usually calls many other functions. This means that we need to inspect all of them to spot any possible throw.

And that't not all! How do we distinguish between the first and the second error (throw)?

We need to create a custom error type instead of using Exception. We then need to remember all possible errors and catch them all:

try {
  program("ab", "abcde");
} on SomeError1 {
  /// ...
} on SomeError2 {
  /// ...
} on SomeError3 {
  /// ...
}

There must be a better way. And indeed there is, fpdart! πŸ‘‡

Option

Option is your friendly alternative to null:

Option<String> getThirdLetterOption(String str) =>
    str.length >= 3 ? some(str[2]) : none();
 
Option<int> searchLetterInStringOption(String str, String letter) {
  int index = str.indexOf(letter);
  return index != -1 ? some(index) : none();
}

Option is a sealed type that can be in 2 states: Some or None.

Some represent the case in which a value is present, while None is the equivalent of null (value missing)

The advantage of Option over null is composition. Option offers an extensive API designed to compose functions together:

int programOption(String source, String search) => 
    getThirdLetterOption(source)
    .flatMap((thirdLetter) => searchLetterInStringOption(search, thirdLetter))
    .getOrElse(() => throw Exception("Error"));

No more nested checks for null. Our program is a linear series of steps without nesting parenthesis.

You can make the code even easier to read by using the Do notation (more details in the next sections πŸ‘‡)

Option<int> programDo(String source, String search) => Option.Do(
      (_) {
        String thirdLetter = _(getThirdLetterOption(source));
        return _(searchLetterInStringOption(search, thirdLetter));
      },
    );

This does not solve the issue of tracking and reporting errors. In fact, we are still required to throw when the value is missing.

We have another solution for this, Either!

Either

Either is your friendly alternative to throw:

sealed class ProgramError {}
class SomeError1 extends ProgramError {}
class SomeError2 extends ProgramError {}
 
Either<ProgramError, String> getThirdLetterEither(String str) =>
    str.length >= 3 ? right(str[2]) : left(SomeError1());
 
Either<ProgramError, int> searchLetterInStringEither(
    String str, String letter) {
  int index = str.indexOf(letter);
  return index != -1 ? right(index) : left(SomeError2());
}

Either is a sealed type that can be in 2 states: Right or Left.

Right represent the case in which a value is present, while Left contains the description of an error (error type)

We now know every possible error without even looking at the source code. All we need is the type signature:

Either<ProgramError, int> programEither(String source, String search) {
  /// This does not matter πŸ’πŸΌβ€β™‚οΈ
}

Just from the return type we know that the function can succeed with a value of type int, or it may fail with a value of type ProgramError.

Furthermore, since ProgramError is a sealed class, we don't even need to know its subtypes! Pattern matching will suggest them to us:

programEither("ab", "abdce").fold(
  /// Error if we forget to handle a subtype in the `switch` πŸ’‘
  (programError) => switch (programError) {
    SomeError1() => /// ...
    SomeError2() => /// ...
  },
  (index) => /// ...
);

There is more.

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 700+ readers.

Side effects

There are many ways to define a side effect. It's easier to understand what a side effect is from some examples:

  • Modify a variable outside of its own scope
int a = 10;
 
int multiplyByTwo(int n) {
  a += 10;
  return n * 2;
}
 
String makeString(int n) {
  String str = "";
  if (n % 2 == 0) {
    /// βœ… This is okay, since the variable is in the local scope
    str += "can be divided by 2\n";
  }
 
  if (n % 3 == 0) {
    /// βœ… This is okay, since the variable is in the local scope
    str += "can be divided by 3\n";
  }
 
  return str;
}
  • Modify a given input value or data structure
List<int> list = [0, 1, 2];
 
int getLast(List<int> list) {
  list.add(10);
  return list.last;
}
  • Logging some text (print)
int multiplyByTwo(int n) {
  print("Value is $n");
  return n * 2;
}
  • Throwing an exception
int multiplyByTwo(int n) {
  if (n == 0) {
    throw Exception("Zero is no good");
  }
 
  return n * 2;
}

A side effects makes the output of a function dependent on the state of the system, which makes testing harder and debugging complex.

When a function has no side effects we can execute it anytime and in any order: it will always return the same result given the same input.

That being said, side effects are necessary and welcomed. The functional programming paradigm aims to have more control on the execution of side effects.

fpdart provides a collection of types designed for side effects, with 2 main objectives:

  • Make side effects explicit using the type signature (similar to Either for errors)
  • Allow more control on the execution of side effects, making your code easier to maintain and test

fpdart has 2 main types for side effect: IO for synchronous operations, and Task for asynchronous (async).

Sync: IO

Examples of synchronous side effects are:

  • Logging (print)
  • Reading an input (stdin.readLineSync)
  • Getting the current date (DateTime.now())
  • Getting a random number (Random().nextDouble())

In all these cases you should wrap your function with the IO type provided by fpdart:

IO<void> printIO(String message) => IO(() {
      print(message);
    });

Now it's clear from the type signature that the function has a sync side effect.

Furthermore, fpdart provides an extensive API to compose side effects, similar to Option and Either.

fpdart also has 2 other types called IOOption and IOEither.

These types join together the features of IO + Option (function with side effect that may return a missing value) and IO + Either (function with side effect that may return an error)

Async: Task

A function has an asynchronous side effect every time it uses async/await (returns a Future).

When a function is async you need to use the Task type instead of the IO.

The API is similar: just wrap the function in a Task.

Task<void> write(int n) => Task(() async {
      final SharedPreferences prefs = await SharedPreferences.getInstance();
      await prefs.setInt('counter', n);
    });

fpdart also has 2 other types called TaskOption and TaskEither.

These types join together the features of Task + Option (function with async side effect that may return a missing value) and Task + Either (function with async side effect that may return an error)

You can read more on the difference between Task and Future, and on how to use TaskEither in the following articles:

There is more.

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 700+ readers.

Dependency injection

In order to make your code easier to maintain and testable we want to be explicit in the type signature: both the return type and the input parameters.

class Console {
  void consolePrint(String message) {
    print(message);
  }
}
 
final console = Console();
 
void program() {
  /// Do something
  
  console.consolePrint("Some debug message here");
 
  /// Do something
}

In this example we are accessing console from the global scope. This makes Console an hidden (or implicit) dependency of program.

This is a problem: what if we want to change the instance of Console when testing? We can't, since we have no way to provide Console to the function.

Solution: make the dependency on Console explicit:

class Console {
  void consolePrint(String message) {
    print(message);
  }
}
 
void program(Console console) {
  /// Do something
  
  console.consolePrint("Some debug message here");
 
  /// Do something
}

Now the function is easier to test:

class MockConsole extends Mock implements Console {}
 
test(
  "testing program",
  () {
    /// Mock the instance of [Console] when testing
    program(MockConsole());
  },
);

Furthermore, we made the function signature more informative. We now know that program is dependent on Console just by reading the definition of the function.

Reader

Now we have a new problem:

void doSomethingElse(String str, Console console) {
  console.consolePrint(str);
 
  /// Return something
}
 
void doSomething(int n, Console console) {
  doSomethingElse("$n", console);
 
  /// Return something
}
 
void program(Console console) {
  doSomething(10, console);
 
  /// Return something
}

Can you see it? We are required to add Console to the input parameters of both program and doSomething, even if both these functions do not use Console at all!

Passing console is required just because a nested call to doSomethingElse depends on it.

This becomes a huge problem pretty fast. Imagine what happens when we have multiple dependencies and not just Console.

fpdart has a solution for this: Reader.

Reader<Console, void> doSomethingElseReader(String str) => Reader(
      (console) {
        /// Do something with `str`
 
        console.consolePrint(str);
 
        /// Return something
      },
    );
 
Reader<Console, void> doSomethingReader(int n) => Reader(
      (console) {
        doSomethingElse("$n", console);
 
        /// Return something
      },
    );
 
Reader<Console, void> programReader() => Reader(
      (console) {
        doSomething(10, console);
 
        /// Return something
      },
    );

By using Reader we do not need to define Console in the input parameters of the functions. The dependency again moved in the type signature:

Reader makes dependencies explicit in the type signature of the function.

Reader<Deps, ReturnType>, where Deps represent the required dependencies, and ReturnType is the type of the value returned by the function.

fpdart provides also other types, like ReaderTask and ReaderTaskEither, which compose together the functionalities of all the types we saw previously

There is more.

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 700+ readers.

Do notation

Since all the types inside fpdart are defined as classes, the standard way of composing function is my chaining calls to the class's methods:

// Combine all the instructions and go shopping! πŸ›’
String goShopping() => goToShoppingCenter()
    .alt(goToLocalMarket)
    .flatMap(
      (market) => market.buyBanana().flatMap(
            (banana) => market.buyApple().flatMap(
                  (apple) => market.buyPear().flatMap(
                        (pear) => Either.of('Shopping: $banana, $apple, $pear'),
                      ),
                ),
          ),
    )
    .getOrElse(identity);

As you can see, every time you need to compose a new function you create a new nested call chain (.flatMap).

This is a common problem in functional programming. It makes your code harder to read, less beginner friendly, and arguably also more complex.

There is a solution: the Do notation!

This is how the same code looks like with the Do notation:

// Combine all the instructions and go shopping! πŸ›’
String goShoppingDo() => Either<String, String>.Do(
      (_) {
        final market = _(goToShoppingCenter().alt(goToLocalMarket));
        final amount = _(market.buyAmount());
 
        final banana = _(market.buyBanana());
        final apple = _(market.buyApple());
        final pear = _(market.buyPear());
 
        return 'Shopping: $banana, $apple, $pear';
      },
    ).getOrElse(identity);

Now the code is linear: one instruction after the other like normal imperative dart code. However, this is still pure functional code!

The _ parameter is a function that extracts the return value from Either, while stopping the execution if the Either returns a failure (Left).

Using _ for naming the input function is a convention.

The Do notation is initialized using the Do constructor (Either.Do in the example).

The Do constructor is available in all the types we discussed in the previous sections.

The Do notation is the recommended way of writing code in fpdart. By using the Do notation your code will become easier to understand and maintain.

There is more.

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 700+ readers.

Immutability and utility functions

That's not all! fpdart provides also some extension methods and utility functions that make writing functional code easier.

fpdart itself does not provide any built-in immutable data structure. It is recommended to use an external package for immutable data structures, for example fast_immutable_collections.

Instead, fpdart provides extension methods on Iterable, List, and Map that implement some immutable methods (head, tail, zip, sortBy, sortWith).

fpdart also provides some extension methods and utility functions on String, DateTime, Random and more:

  • dateNow (IO<DateTime>)
  • now (IO<int>)
  • random (IO<double>)
  • randomBool (IO<bool>)
  • randomInt(int min, int max) (IO<int>)

Furthermore, fpdart implements some extension methods for functions (negate, and, or, and more):

bool isEven(int n) => n % 2 == 0;
bool isDivisibleBy3(int n) => n % 3 == 0;
 
final isOdd = isEven.negate;
final isEvenAndDivisibleBy3 = isEven.and(isDivisibleBy3);
final isEvenOrDivisibleBy3 = isEven.or(isDivisibleBy3);
final isStringWithEvenLength = isEven.contramap<String>((n) => n.length);

Another useful feature of fpdart is currying:

int sum(int n1, int n2) => n1 + n2;
 
/// Convert a function with 2 parameters to a function that
/// takes the first parameter and returns a function that takes
/// the seconds parameter.
final sumCurry = sum.curry;
 
/// Function that adds 2 to the input
final sumBy2 = sumCurry(2);
 
/// Function that adds 10 to the input
final sumBy10 = sumCurry(10);
 
/// Apply the function directly inside `.map`
final list = [0, 1, 2, 3];
final listPlus2 = list.map(sumBy2);
final listPlus10 = list.map(sumBy10);

You can inspect the full API in the reference documentation


fpdart v1 is out this week after months of development and testing.

The functional programming community is growing fast, not just in dart but in many other languages and technologies.

As the community grows, new innovative ideas and implementations are released everywhere. fpdart is no different.

Looking at the future for fpdart (v2), the objective is to explore new ideas to make the API more clear and easier to use.

Making functional programming more accessible and beginner friendly.

This post was just an overview of what fpdart has to offer. More documentation and tutorials are already available on my blog, and more are coming in the upcoming months.

You can subscribe to my newsletter here below for more frequent updates, exclusive content, code snippets, and more πŸ‘‡

Happy coding!

πŸ‘‹γƒ»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 700+ readers.