Originally posted on: Big ball of mud
Since the beginning of the "computer programming era", developers had searched for a solution for one of the biggest mistake made in computer science, the invention of the null reference. Since functional programming became mainstream, a solution to this problem seems to arise, the use of the optional type. In this post, I will analyze how this type is used in different programming languages, trying to understand the best practices to apply in each situation.
Returning the nothing
Let's start from a concrete example. Think about a repository that manages users. One classic method of such type is the method def findById(id: String): User
, whatever an id
could be. This method returns the user that has the given id, or nothing if no such user exists in your persistence layer.
How can we represent the concept of nothing? It is common to use the null reference to accomplish this task.
val maybeUser = repository.findById("some id")
val longName = maybeUser.name + maybeUser.surname
The problem is not in returning a null reference itself. The problem is using a reference containing a null
. The above code, written in Scala, rises a NullPointerException
during the access of the methods name
and surname
if the user
reference is equal to null
.
So, what's a possible solution? A first attempt can be to return a List[User]
. The signature of the method becomes the following, def findById(id: String): List[User]
. An empty list means that there is no
user associated with the given id
. A not empty list means that there is a user associated with the given id
. The problem is that the user is exactly one, whereas the semantic of lists if to store zero or more elements.
So using a list seems to be convenient, but not semantically correct. The use of an empty list to represent the nothing produces programs that are hard to read (violating the Principle of least astonishment) and to maintain.
However, the idea behind the use of lists is perfect. How can we refine it?
The Option type
The answer to our question is in the type that Scala calls Option
. Other programming languages call this type in different ways, such as Optional
in Java, or Maybe
in Haskell.
The Option[T]
type is just like a list that can be empty or can contain exactly one element. It has two subtypes that are None
and Some[T]
. So, an Option
object that contains something is an instance of the Some[T]
subtype, whereas an empty option is an instance of None
. There is only one instance of the None
type that in Scala is defined as an object
.
Option
is indeed an Algebraic Data Type, but the definition of such a concept is behind the scope of this post.
It is possible to use the factory method Option(value: T)
, declared in the Option
object
to build a new instance of an Option
class. The factory method will create a new Option
instance: If value
is equal to null
, then the None
object is returned, Some(value)
otherwise.
Using the Option
type, our previous method changes its signature in def findById(id: String): Option[User]
. Ok, interesting. However, how can I use the user value contained inside the option type? There are many ways to do that. Let's look at them.
Getting the value out of the option type
One of the possible choices is to try to get the value out of the option. The API of the Option
type lists the methods isDefined
and get
. The first check if an option contains a value, and the second allows you to extract that value. The usage pattern raising from the use of the above methods is the following.
val maybeUser: Option[User] = repository.findById("some id")
var longName: String;
if (maybeUser.isDefined) {
longName = maybeUser.get.name + maybeUser.get.surname
}
Despite the use of a var
reference, the above pattern is to avoid. If you call the get
method on a None
object, you will obtain an error at runtime. So, you don't improve the code that much from the version that checks for the null
directly.
For this reason, the Scala Option
type provides a variant of the get
method, which is getOrElse
. This method allows you to provide a default value to return in case of an empty option. Imagine that our User
type provides a gender: Options[String]
method, which eventually returns the gender of a user. It is possible to use the following safer pattern using getOrElse
.
val maybeUser: User = // Retrieving a user
val gender: String = maybeUser.gender.getOrElse("Not specified")
Using pattern matching
In Scala, it is possible to use pattern matching on Algebraic Data Types. The Option
type is not an exception in this sense. Although it is not such idiomatic, this approach does what it promises us — raw and straightforward.
val maybeUser: Option[User] = repository.findById("some id")
val longName: String = maybeUser match {
case Some(user) => user.name + user.surname
case None => "Not defined"
}
Using pattern matching is a little more verbose than using the next approach I am going to show you, but it is still more elegant than checking the existence of a value using the isDefined
method.
Using the Option type as a list
The suggested approach by the Scala community is to use the Option
type, just as if it was a list. This approach takes advantage of the fact that the Option
type is indeed a monad (also, in this case, explaining what a monad is in functional programming is behind the scope of this post).
The Option
type, like lists, exposes a lot of useful methods to transform e manage the Option
inner value, without even the need to extract it. Among the others, such methods are the map
, flatMap
and filter
functions.
The map
method allows you to transform the value owned by an Option
object if any.
val maybeUser: Option[User] = repository.findById("some id")
val longName: Option[String] = maybeUser.map(user => user.name + user.surname)
The flatMap
method allows you to compose functions that in turn, return an object of type Option
.
val maybeUser: Option[User] = repository.findById("some id")
// Using a simple map function, we obtain an Option[Option[String]]...uhh, ugly!
val gender: Option[String] = maybeUser.flatMap(user => user.gender)
Finally, the filter
method allows you to discard any value that does not fulfil a given predicate.
val maybeUser: Option[User] = repository.findById("some id")
val maybeNotARiccardo: Option[User] = maybeUser.filter(user => user.name != "Riccardo")
All the above methods, if invoked on a None
object, return None
automatically, preserving any method call concatenation.
Note that we can combine the use of the map
, flatMap
and filter
method using for-comprehension, giving to our code a look more like Haskell.
val maybeUser: Option[User] = repository.findById("some id")
val maybeNotARiccardo: Option[User] = maybeUser.filter(user => user.name != "Riccardo")
val maybeTheGenderOfANoRiccardo: Option[String] = maybeNotARiccardo.flatMap(user => user.gender)
The above code can be rewritten using a simple for...yield
expression.
val maybeTheGenderOfANoRiccardo: Option[String] =
for {
user <- repository.findById("some id")
gender <- user.gender if user.name != "Riccardo")
} yield (gender)
Last but not least, if you need to apply some side-effects if a value is present in an Option
object, you can use the foreach
method. This function cycles on the single value of the option, if present, and executes a procedure.
val maybeUser: Option[User] = repository.findById("some id")
maybeUser.foreach(user => println(s"$user.name $user.surname"))
Option type in other languages
The Option
type is not present only in Scala. Many programming languages widely use it.
Java
Since Java 8, the language introduced the Optional<T>
type in the mainstream JVM language. It is very similar to its Scala cousin.
Also, the Java API defines different factory methods to build a new instance of Optional
: Optional.of
which throws an exception if the given value is null
, and Optional.ofNullable
, that returns an Optional.empty
in case of null
value.
A small difference if the behaviour of the map
method. If the function used by map
returns a null
value, the method interprets this as the Optional.empty
value. The map
method behaves like the flatMap
method, in this particular case.
Optional<User> maybeUser = repository.findById("some id");
Optional<String> maybeUserName = maybeUser.map(user -> {
if ("Riccardo".equals(user.getName()) {
return user.getName();
} else {
return null;
}
});
The above code returns an Optional.empty
value if the name of the user is equal to the String
"Riccardo"
.
Another interesting method of the Java Optional
type is orElseThrow
. This method allows us to choose which exception to rising in case of an empty Optional
object.
It worth reporting the answer that Brian Goetz gave to the StackOverflow question, Should Java 8 getters return optional type?.
NEVER call
Optional.get
unless you can prove it will never benull
; instead use one of the safe methods likeorElse
orifPresent
. In retrospect, we should have called get something likegetOrElseThrowNoSuchElementException
or something that made it far clearer that this was a highly dangerous method that undermined the whole purpose of Optional in the first place. Lesson learned.
Kotlin
The Kotlin programming language does not have built-in support for something similar to the Scala Option
type or the Java Optional
type. The reason why is mainly that the language support compile-time null checking.
In Kotlin every type has two variants. Continuing our previous example, Kotlin defines both the User
, and the User?
type. A reference of the first type cannot be null
, whereas an instance of the User?
type can also hold the null
value. Any non-nullable type is a child type of the corresponding nullable type.
In other words, Kotlin attempts to solve the problem by forcing you to handle null references. Kotlin forces you to handle the exception or to take full responsibility. For example, you can't use the dereferencing operator, the .
(dot), if you are managing nullable type. The following code won't even compile.
val maybeUser: User? = repository.findById("some id")
val longName = maybeUser.name + maybeUser.name
Kotlin tries to prevent NullPointerException
for you. The safe call operator, ?.
must be used in this case. It checks if the reference is null
, and if it is not, it calls the method on it, propagates the null
value otherwise.
val maybeUser: User? = repository.findById("some id")
val longName: String? = maybeUser?.name + maybeUser?.name
The power of the safe call operator shines with chained methods calls. In this case, it also reminds very strictly the use of the methods map
and flatMap
defined in the Option
type.
val maybeGender: String? =
repository.findById("some id")?.gender
The above method is equal to the following Java counterpart.
String maybeGender =
repository.findById("some id")
.flatMap(user -> user.getGender())
.getOrElse(null);
If you don't mind about NPEs, you can always use the !!.
operator, that does not perform any check on reference nullability.
Finally, also Kotlin defines the same behaviour of the orElse
method in Scala or Java for nullable types, using the Elvis operator, ?:
. If we want to return a particular value of the gender if something is null
during the gender resolution process, we can proceed as the following code shows.
val maybeTheGender: String =
repository.findById("some id").?gender ?: "Not defined"
As you can see, the type of the last maybeTheGenderOfANoRiccardo
variable is not nullable anymore, because the elvis operator eliminates the possibility for the reference to be null
.
Conclusions
In this post, we analyzed a modern solution to deal with the null
value and in general, the absence of information. We started to see how the Scala programming language defines Option[T]
type, listing the best practice of its use. Then, we saw how the good old Java approaches to the problem, the Optional<T>
type, concluding that it is very similar to the solution proposed in Scala.
Finally, we looked at a different solution, the one proposed by Kotlin. Kotlin does not have any optional type, because it tries to handle the nullability of references at compile-time. Although this approach is exquisite and as powerful as the approaches proposed in Scala and Java, it poorly composes with the other constructs used in functional programming. For this reason, Kotlin has its library for functional structures, such as the optional type, which is ARROW.
Bonus: Are you planning to use an option value as a parameter for a method? Please, don't do that (Why should Java 8's Optional not be used in arguments).
Top comments (0)