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
:
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 π€©
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
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
maythrow
. 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 asealed
type that can be in 2 states:Some
orNone
.
Some
represent the case in which a value is present, whileNone
is the equivalent ofnull
(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 asealed
type that can be in 2 states:Right
orLeft
.
Right
represent the case in which a value is present, whileLeft
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 π€©
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
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 calledIOOption
andIOEither
.These types join together the features of
IO
+Option
(function with side effect that may return a missing value) andIO
+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 calledTaskOption
andTaskEither
.These types join together the features of
Task
+Option
(function with async side effect that may return a missing value) andTask
+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 π€©
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
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>
, whereDeps
represent the required dependencies, andReturnType
is the type of the value returned by the function.
fpdart
provides also other types, likeReaderTask
andReaderTaskEither
, which compose together the functionalities of all the types we saw previously
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.
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 fromEither
, while stopping the execution if theEither
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 π€©
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
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!