Functional programming has been garnering more attention in recent years, for good reason: it’s a tool for writing more predicatable and readable code. I won’t go into the details here - many, many, articles have covered that already. If you are looking for an intro, though, this reddit comment may prove helpful.
This post explains how I’ve incorporated aspects of functional programming into my Flutter projects with Fpdart. Each section includes an example of how a feature from the package can be used, as well as any potential downsides and considerations.
Option
More control of null values
If you’ve used Riverpod, the following code will feel very familiar - let’s start with an immutable Person
class that includes a name and, optionally, an age:
class Person {
final String name;
final int? age;
Person(this.name, this.age);
}
Next, we’ll add a copyWith
method to easily create a new instance of Person
with some of the fields changed:
class Person {
final String name;
final int? age;
Person(this.name, this.age);
Person copyWith({String? name, int? age}) {
return Person(
name ?? this.name,
age ?? this.age,
);
}
}
Now, take a look at the following code:
var steve = Person('Steve');
steve = steve.copyWith(age: 30);
This creates a person, steve
, with no specified age, then creates a new instance of Person
with the same name but an age of 30. This is a common pattern in Flutter.
What if we wantted to change the age back to null? Your first thought may be the following:
steve = steve.copyWith(age: null);
However, age
will remain 30, because the copyWith
method above only changes the value if the new value is not null.
print(steve.age); // prints "30", even after the copyWith call
The Option
type from Fpdart helps to address this issue. It’s a wrapper around a value that can either be Some
or None
, which can be used instead of nullable types.
// Creating an Option with a value of `None`
Option<String> data = None();
// Updating the value to `Some`
data = Some('Hello, world!');
If we were to use Option
in the Person
class above, we could change the age
field to be of type Option<int>
:
class Person {
final String name;
final Option<int> age;
Person(this.name, [this.age = None()]);
}
Next, we can add the copyWith
method again, using the Option
type:
class Person {
final String name;
final Option<int> age;
Person(this.name, [this.age = None()]);
Person copyWith({String? name, Option<int>? age}) {
return Person(
name ?? this.name,
age ?? this.age,
);
}
}
Now, it is possible to change the age from a value to None
:
var steve = Person('Steve');
steve = steve.copyWith(age: Some(30));
print(steve.age); // prints "Some(30)"
steve = steve.copyWith(age: None());
print(steve.age); // prints "None"
To actually use the value contained in an Option
, there are a few methods available:
final option = Some(5); // Implicit `Option<int>`
// Returns the value if it is `Some`, otherwise returns the default value
// Similar to the `??` operator
final value = option.getOrElse(() => 0); // value = 5
final triple = option.match(
() => 0, // Called if `None`
(value) => value * 3, // Called if `Some`
); // triple = 15
final nullable = option.toNullable(); // nullable is an `int?` with a value of 5
Option
can also be converted to an Either
, another type from Fpdart (explained in more detail below):
// Converts to an `Either<String, int> with a value of `Right(5)`
// If the value was `None`, the `Either` would be `Left('No value')`
final either = option.toEither(() => 'No value');
Many more methods can be found in the Fpdart docs.
Downsides
Using Option
can be a bit more verbose when compared to using nullable types. It adds a small amount of boilerplate and means you can not use the ??
operator to set a default value (though getOrElse
is a similar alternative). In my experience, though, the benefits far outweigh the downsides.
Either
Error handling without exceptions
Error handling in Dart is painful. The main problem for me is that it’s unclear as to whether a function can throw an error or not unless you read the implementation or see it mentioned in the docs. Combined with the somewhat awkward syntax for try
/catch
blocks, it is far from a great experience.
Either
is a type from Fpdart that can be used to handle errors in a more functional way. It’s a wrapper around a value that can either be Left
or Right
, which can be used instead of throwing exceptions. Typically, Left
is used to represent an error, and Right
is used to represent a successful result.
Let’s say we have a function that can throw an error called fetchFromDatabase
. Instead of just hoping the error is caught, we can wrap the result in an Either
, where Left
will be a string, used to represent an error, and Right
will be an int, used to represent a successful result that was fetched from the database:
Either<String, int> fetchFromDatabase() async {
try {
// This could be a firebase call, a network request,
// or anything else that could throw an error
final int data = await database.fetchData();
// Assuming the data was successfully fetched,
// we return it wrapped in `Right`
return Right(data);
} catch (error) {
// If an error was thrown, we return it wrapped in `Left`
return Left(error.toString());
}
}
It’s now very clear that fetchFromDatabase
can throw an error, and it’s also much easier to handle the with pattern matching:
final result = await fetchFromDatabase();
// Pattern matching
result.match(
(error) => print(error), // Called if `Left`
(data) => print('Data found: $data'), // Called if `Right`
);
Either
can also be converted to an Option
(explained in more detail above):
final either = Either<String, int>.of(5);
// Converts to an `Option<int>` with a value of `Some(5)`
// If the value was `Left`, the `Option` would be `None`,
// no matter what the value was
final option = either.toOption();
Downsides
Due to exceptions being built into the language and found in many libraries, it can be difficult to be consistent with how you handle them when you introduce Either
. It is possible to wrap the result of any throwing functions in an Either
, but this will add more boilerplate to your code.
Tuple
Multiple values without a class
Returning multiple values from a function can be accomplished by creating a convenience class, but this tends to add unneeded verbosity. Tuple2
(because it holds 2 values) is a type from Fpdart that can be used to return multiple values from a function without creating a class.
In this example, we are fetching data from a database, and we want to return the data and the time it took to fetch it. We can use Tuple2
to do this:
Tuple2<int, Duration> fetchFromDatabase() async {
final stopwatch = Stopwatch()..start();
// This could be a firebase call, a network request, etc.
final int data = await database.fetchData();
stopwatch.stop();
return Tuple2(data, stopwatch.elapsed);
}
The values can then be accessed using the first
and second
properties:
final result = await fetchFromDatabase();
print(result.first); // Prints the data
print(result.second); // Prints the duration
Downsides
It can be unclear as to what the values represent, so it’s best to use a class if you need named parameters. Tuple2
is also limited to holding 2 values, so it’s not a good fit for more complex use cases.
Note that Dart 3 will introduce the Record
type, which provides similar functionality to Tuple2
but with more flexibility, including more than two values and named parameters.
The Rest
Fpdart includes over 20 types and extensions, only three of which have been covered here. While the other types certainly have uses, I haven’t found many situations in which my code has been signifigantly improved. Either way (pun intended), you can view examples for all of the types in the Fpdart docs and in the example folder of the GitHub repository. 🐟