Close your eyes for a moment.
You are in the first year of your IT career, whichever it is, there is no discrimination here. You go to your first programming class and your teacher (let's identify him as a man) begins to explain what an algorithm is and how they exist everywhere even if they aren't noticeable with the naked eye.
Suddenly he introduces this word of "programming" and mentions to you that there are several types, styles, or as it sounds more intellectual: paradigms. He tells you that they're going to learn the structured paradigm because it's your first class. You hear that he also mentions the object-oriented paradigm, and calm down, you will see that a semester later.
There are several that he mentions that probably won't teach you in college, at least that's the way it happened to me. One of those was functional programming or the functional paradigm. This paradigm is sometimes neglected because not all languages allow it to be implemented as it should.
As the title says, this article is focused on the Dart language and also on package:oxidized
. There are other packages for functionally programming in Dart but trusted sources have recommended me oxidized
.
The functional paradigm
From that introduction, you can intuit that I'm not an expert in this paradigm, but we are going to focus on the basics so that you can understand the great tricks that can be achieved with it.
Functional programming has the characteristics of being declarative and also treating functions as a primitive data type, or as it sounds nicer, first-class citizens.
Those who already know Dart, know that it fits very well with these characteristics. We can send a function to a class or another function as a property or argument.
void main() {
final greeting = (String name) {
return 'Hi $name';
};
saySomething(greeting);
}
void saySomething(Function(String) message) {
print(message('Marcos'));
}
// Hi Marcos
There we create an object that is a function and we can execute it if we pass it as an argument to another. Compatibility with the paradigm, checked.
In itself, functional programming has an origin closely linked to mathematics since it uses this scientific foundation to be able to process more computationally complex tasks thanks to the replacement of loops, which increase complexity many times, by functions that accept other functions as arguments or they return a function as a result. The latter are called higher-order functions.
Advantages
The big question is: what is the advantage of incorporating this paradigm into our code?
I would summarize: it allows us to make code more concise, it reduces the complexity when reading it by not having several control statements in it. It makes testing it much easier as we have function-specific results. The coolest thing, it can be easily combined with other paradigms.
Disadvantages
Not everything is good, there are some problems when we need a variable state since results are returned, this is where combining it with other paradigms is very useful. It can be slow when going into many recursions.
In general, it is not the best alternative for all tasks.
Oxidized
This package contains many tools that make it easy for us to incorporate functional programming into our project. We go by parts.
First, as a basis, we are going to have a Person
entity to exemplify some use cases where we can use functional programming tools.
class Person {
const Person({
required this.age,
required this.name,
});
final int age;
final String name;
}
Result
The first class that is super useful is Result
. It's an abstract class that represents success or failure, it can return the expected result of an operation or the exception it threw. For example:
Result<String, Exception> getPersonName(Person person) {
final returnName = Random().nextBool();
return Result.of(() {
if (returnName) {
return person.name;
} else {
throw Exception();
}
});
}
In this case, we define a function that returns a Result
that can contain a value of String
or Exception
. So our function receives a Person
object and based on a random value that we generate, we send a response.
If you notice, we return the name wrapped in a Result.of
constructor. This operation can be seen as a try-catch block, but we can forget about handling the exception here and focus on acting upon the err
case from the result.
void main() {
final person = Person(age: 22, name: 'Marcos');
final result = getPersonName(person);
result.when(
ok: (name) => print(name),
err: (e) => print('Something went wrong... $e'),
);
}
// Something went wrong... Exception
// Marcos
Now that we instantiate the class and pass it as an argument to our function, something interesting happens. Since our variable can adopt two possible values of Result
, we have a very useful method called when
that gives us access to two functions that return a data type each to perform some specific action, such as assignments or the execution of another function that needs the argument that it returns.
If that sounds a bit confusing, see when
as an if-else
for the two possible data types the function can return.
This class is very useful when we call a third-party API that we don't know very well to catch any unknown errors and return them in the function.
Option
Option<int> getAge(Person person) {
final returnAge = Random().nextBool();
if (returnAge) {
return None();
} else {
return Some(person.age);
}
}
One very similar to Result
is Option
. Sometimes we don't want to return two possible values, and here you will say "ah, that's a normal function" but Option
allows us more than that.
First, it allows us to adapt if
logic to decide what to do based on the result obtained. It can be used to return validation of the execution of some operation.
void main() {
final me = Person(age: 22, name: 'Marcos');
final option = getAge(me);
option.when(
none: () => print('None!'),
some: (age) => print('Aha! Your age is $age'),
);
}
// None!
// Aha! Your age is 22
We have the new classes that implement it: None
and Some
. None
allows us to return an object that represents that the expected action cannot be performed and Some
contains the value that the operation performed was expected to return.
See None
as a missing value that could not be returned. This helps us to handle general errors and the composition of our code in a more expressive way.
Oh right...
Result
and Option
are structures that are called monads. These can be applied to data types to follow a specific sequence of steps. Like when we obtain one of two specific values and execute some action based on what we receive.
The monad types are akin to the sum types found in languages such as OCaml and Rust, as found in package:oxidized
's documentation. They encourage safer code by making errors and possible null values (such as None
) a necessary part of the code flow, requiring you to deal with these cases appropriately.
Closing remarks
You can go much deeper into other functional programming packages if you want. Some of them like package:dartz
contains a lot of functionality that comes in handy if you are into math and categorization theories. I consider myself a bit more pragmatic, I quite like this style and I share these small tips with you because I consider it to be useful in any type of project.
Likewise, the monads that we saw have more methods that can be useful to them. They remind me a lot of the methods we have in the classes that we do with package:freezed
but applied to functions --yeah, functional programming-- and their return values.
Very useful to handle different scenarios in your projects, especially possible errors or unforeseen results.