Why do we like functional programming so much? The simple answer is that it makes the day-to-day life of developers easier—in this article we will explain how.
If you think about it, most programs model business rules. Here are some typical examples:
- Only administrators should be able to make posts to a LinkedIn Page
- A user can only create an account if she has specified a valid email address
- You should not be able to add a value in US dollars to a value in Euros
Often these rules can be encoded in the type system of the language. For example:
- The function for creating Page posts could require an
Administrator
object (a subclass ofUser
) - The “create account” function could accept an
EmailAddress
(which might have avalidated
property) - Each currency in the system could have its own class, derived from an
abstract
Currency
class
This frees the programmer from having to think about preventing these errors, because the type system makes sure they can’t happen! (This is often referred to as making illegal states unrepresentable)
However, there’s a problem with this approach, at least in mainstream OOP languages: In Java or C# creating a new class is a lot of work! You have to define methods for construction, equality, stringification, serialization, and so on. Sure, you can have your IDE generate the code, but you still have to manage all that boilerplate.
For example, here’s how we might write a very simple representation of an email address in C#:
public class EmailAddress
{
public string Address { get; }
public EmailAddress(string address)
{
if (address == null)
{
throw new ArgumentException("Email address cannot be null.");
}
Address = address;
}
public override bool Equals(object obj)
{
if (obj is EmailAddress other)
{
return string.Equals(Address, other.Address);
}
return false;
}
public override int GetHashCode()
{
return Address.GetHashCode();
}
public override string ToString()
{
return Address;
}
}
(Implementing email address validation is left as an exercise to the reader)
The programmer is stuck in an unenviable position: Either put in the hard work to encode the business rules in the type system, or fall back to using primitive types and ensuring correctness by hand; probably with the help of a lot of unit tests.
But why is all this boilerplate needed? The core problem is that an object is
supposed to encapsulate state; the outside world should not know (nor need to
know!) what the insides of the object looks like. However, this also means that
the outside world cannot draw any conclusions about an object or a class. For
example, what does it mean for two Administrator
values to be equal to each
other? Are they equal only if they reference the same object, or are they equal
if they reference the same user? The world outside the object can’t tell, and
so it falls to the object (and by extension, the programmer) to create a
definition of equality for each class.
Functional programming does away with all that; just like in mathematics, a number is a number and a string is a string. If they have the same value, they are, for all intents and purposes, the same. This also means that equality, stringification, comparison etc become trivial; if a value is just a value, we can reuse the same logic for all instances of that value. This also means that creating new types become a trivial exercise. For comparison, here’s the canonical definition of an email address in F#:
type EmailAddress = EmailAddress of string
If something is easier, people tend to do it more often. Functional programming makes it easy to encode domain rules in the type system, so programmers are nudged to do it more. In the end, this creates software that is more correct, easier to maintain and more fun to write.