Introducing FP step by step
In the last few years, Functional Programming has become more and more popular. Thanks to hybrid languages like Scala, many developers have started to appreciate the advantages of this style of programming.
There are, however, some drawbacks with FP that hold back many teams from introducing it. The main issues I have seen are all related to the steep learning curve: many concepts are a bit scary at the beginning and it’s often hard to introduce them in an old project.
I think a big issue is also the fact that most articles and talks are about advanced concepts like FreeMonads, MonadsTransformers, Kleisli, Coyoneda… and many other things with weird names that tend to scare newcomers.
The truth is, although these concepts are really interesting and fun to play with, the most useful concepts for the day-to-day work are the basic ones. They’re often overlooked but they’re the ones with the biggest advantages.
In fact, if you’re working in a language like Scala which was not designed to be used as a pure FP language, I think some of these advanced concepts are not worth using at all, because in many cases they bring more problems than advantages.
So, let’s see what these basic concepts are, and why they’re so useful:
- Use functions for code reuse and compositions
- Separate data and functions
- Use types for correctness
- Don’t use mutable states
- Don’t throw exceptions, use appropriate data structures
- Understand the Monads and don’t be afraid of them
- Use typeclasses for polymorphism
As you can see, besides the last two, most of these don’t require any extra knowledge or any particular language feature to be applied and can give amazing results.
In this article, we’re going to focus on the first two. From their names, you probably guessed they’re the most important :)
Use functions for code reuse and composition
There’s this popular idea that OO is great for code reuse and composition. For instance, using inheritance we can save a lot of code by putting all the common logic in a base class and then extending it to define the specific cases. This actually does work, but it has a lot of pitfalls that we don’t really need to deal with since we can achieve the same things in a much simpler way just using functions.
To explain what that means, let’s go straight to an example:
object OO {
trait Op {
def op(i1: Int, i2: Int): Int
def exec(i1: Int, i2: Int): String = {
s"The result is: ${op(i1, i2)}"
}
}
object Sum extends Op {
def op(i1: Int, i2: Int): Int = i1 + i2
}
object Mult extends Op {
def op(i1: Int, i2: Int): Int = i1 * i2
}
}
object TFP {
def exec(op: (Int, Int) => Int)(i1: Int, i2: Int): String = {
s"The result is: ${op(i1, i2)}"
}
val sum: (Int, Int) => String = exec(_ + _)
val mult: (Int, Int) => String = exec(_ * _)
// alternative implementation without currying
def mult2(i1: Int, i2: Int): String = exec(_ * _)(i1, i2)
}
val r1 = OO.Sum.exec(3, 4)
val r2 = OO.Mult.exec(3, 4)
val r3 = TFP.sum(3, 4)
val r4 = TFP.mult(3, 4)
assert {
r1 == r3 && r2 == r4
}
This is a very common example. In order to abstract over some logic, we create base traits/classes with some abstract methods which then we use inside other methods, and then we implement these methods in subclasses to define the specific behavior for that instance.
This is extremely error-prone. This example is trivial but when we start to add layers of classes and methods, it becomes very easy to break the code because something in the hierarchy could override something unexpected.
The same goal can be easily achieved by taking a function as a parameter. We can define our generic functionality, exec
in this case, and then define specific cases like mult
or add
without the need to create a class hierarchy.
As you see in the example, we’re using something called currying. The exec
function takes two parameter groups. If we pass only the parameters of the first block, this will return a new function that takes only the parameters of the second block. This is great to create functions that are more generic and then create specialized versions of them.
In this second example, we’re going to see something even worse than in the first. The first one is actually a pattern that’s very popular in Java, but there was a good reason to do that since Java didn’t have lambdas until version 8 which meant we couldn’t take a function as parameter and do what we’ve seen above. However, in Scala it’s very common to use traits to reuse
code even when there is no reason at all:
object OO {
trait Fooable {
def foo[T](t: T): String = {
s"foo: $t;"
}
}
trait Barable {
def bar[T](t: T): String = {
s"bar: $t;"
}
}
trait FooBarable extends Fooable with Barable {
def fooBar[T](t: T): String = {
val f = foo(t)
bar(f)
}
}
object Model1 extends FooBarable
object Model2 extends FooBarable
}
object FP {
def foo[T](t: T): String = {
s"foo: $t;"
}
def bar[T](t: T): String = {
s"bar: $t;"
}
def fooBar[T]: T => String = foo _ andThen bar
def fooBar1[T]: T => String = bar _ compose foo
}
assert {
OO.Model1.fooBar(4L) == OO.Model2.fooBar(4L) &&
FP.fooBar(4L) == FP.fooBar1(4L) &&
OO.Model1.fooBar(4L) == FP.fooBar(4L) &&
FP.fooBar(4L) == "bar: foo: 4;;"
}
The traits Fooable
and Barable
contain two functions which we now want to use in our Model
objects. It seems very common to put functions inside traits and then mix them together to reuse those functions, but it’s a bad practice in many ways.
Mixing traits has a compile-time and runtime cost, and is also semantically wrong because we’re implying that our class is an instance of Foo and Bar. It’s much better to define the functions inside an object and just import it, without having to extend anything.
Separate data and functions
This second concept is strictly connected to the first one. It’s a very common practice in OO to put together data and functionalities in the same
class. For instance:
case class Person(name: String, surname: String, age: Int) {
def print: String = s”$name $surname, age: $age”
}
This seems to be the obvious thing to do when we write functions that access the class fields. We just put them inside the class itself, but it’s a bad practice for a number of reasons.
When we work on a big project with different modules we often want to share our data definitions between our modules because the entities that represent our data are often the same for every part of the project and the only thing that changes between the different modules is the logic we apply to them.
What we usually do is we start to share our data classes, and this is good because we want a consistent representation of our data between different modules without copying those same classes everywhere. The problems start when we put logic inside these models because every module has different requirements and the models become monsters, full of functions that do similar things in different ways often reusing each other and by this point, we can’t move things around anymore. Functions have dependencies on each other and on the class fields, so trying to move something means moving a lot of code.
A much better approach is to totally separate the data definition and the functions that use that data, using (case) classes only as data containers and objects to define the functions so the objects are for us basically just namespaces.
Let’s start with a simple refactoring of our Person
:
case class Person(name: String, surname: String, age: Int)object PersonPrinter {
def print(person: Person): String =
s”${person.name} ${person.surname}, age: ${person.age}”
}
This probably seems more verbose but it has a big advantage: the data and behaviors are not related anymore, so moving the print function around is much simpler now (especially when you have some dependencies to that function).
You may be thinking that now you have to repeat person
every time you access a field and that that’s verbose especially when you have many of them. Well, remember that in Scala you can import instances, so we can rewrite this code:
object Person {
def print(person: Person): String = {
import person._
s”${name} ${surname}, age: ${age}”
}
}
Now we have a concise syntax again, but with all the advantages of splitting data from functions.
The example is still a bit too easy, so let’s develop it a little bit in order to clarify why this is really a better approach. Imagine we have two projects, p1
and p2,
that are using the Person
class but that need to print it with some more added information:
case class Person(name: String, surname: String, age: Int) {
// needed from "common" project
def print: String = {
s"$name $surname, age: $age"
}
// needed from "p1" project
def printWithPhone(phone: String) = {
print + ", phone: $phone"
}
// needed from "p2" project
def printWithAddress(address: String) = {
print + ", address: $address"
}
}
This is what usually happens. All the functions needed for the Person
class end up in the class itself, and we start to create dependencies between them so after a while, the class becomes very big and full of unrelated functions. In that case, if we want to move something it becomes very hard. Even with just these three methods if we wanted to move the print
method we’d also have to move the others, so just imagine what it would be like if we had 100 methods!
Let’s change this code to make it maintainable:
// common
case class Person(name: String, surname: String, age: Int)
object Person {
def print(person: Person): String =
s"${person.name} ${person.surname}, age: ${person.age}"
}
// p1
object PersonHelper {
import Person._
def printWithPhone(person: Person, phone: String) = {
print(person) + ", phone: $phone"
}
}
// p2
object PersonHelper {
import Person._
def printWithAddress(person: Person, address: String) = {
print(person) + ", address: $address"
}
}
Now things look much better. We have a Person
class that defines the data, we have the companion object with a standard print method that isn’t project-specific, and each project defines its own helper (or Service, Rich, or whatever you like) with all the specific methods you need. Imagine you want to move the print
method. All you have to do is change the imports and you’re ready to go with nothing else to change.
As you can see, FP isn’t necessarily about using complicated concepts and advanced language features. We can start from easy concepts and just get used to reasoning using functions and data rather than classes, interfaces, inheritance, etc.
This approach makes it easy to move a codebase as well. We can start simplifying classes, removing unnecessary traits and moving the data to a different place rather than the functions, step by step.