Functional Programming in Dart

Functional Programming in Dart

With Oxidized

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

https://cdn.hashnode.com/res/hashnode/image/upload/v1644900119562/4dVEWw7A2.gif

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.

https://cdn.hashnode.com/res/hashnode/image/upload/v1644900121795/t3hdidRMx.gif