Functional programming tries to use return types to convey information. This is particularly important when it comes to errors and failures. Procedural approaches have used exceptions, sentinel values, mutable parameters and return codes to convey ‘extra’ information about the result of a call. However, not all of these are appropriate in a pure functional world. So we prefer types as the fundamental way of carrying knowledge through a program. Here I want to explore some of the fundamental functional types which form the building blocks of functional programming. Examples come from the Scala language.
Option
The first functional type to tackle is the Option. We use it as an alternative to the sentinel null value and the required null-checks and guards which are needed to use it correctly.
An Option[A] is the functional way of representing a nullable value. It represents an object which might not have any value or might have a single value of type A. The procedural world has woken up to the problems associated with the null value and Option is creeping into more object-oriented languages these days either in widely used libraries (e.g. Guava) or as part of the core language library itself (e.g. JDK8).
The difficulty with null is primarily related to its type. null is not generally a valid value of the type of the variable and so accessing the variable through that type’s API when its value is null is an error. To operate effectively in a language with nullable variables you have to first discover whether the value is null and then act appropriately. There’s no way of telling whether a given variable is actually nullable or not. To work with them you have to make some reasonable assumptions or work on convention.
Using Option can make it clear that the value in question might not represent a real value and must be validated before being treated as a pure value. The addition of a map() method in its API allows for accessing the value in a null-safe fashion. map takes a f: A => B and produces an Option[B] which is itself either null or the value of f(a), where a is the contained value.
In most functional languages (and, specifically, in Scala) there are exactly 2 subtypes of Option[_], which can be valuable when matching. The first is None and the second is Some[_]. As might be expected, None is the null type and Some[A] represents a single value of type A which is not null.
What Option actually does for us is to remove the need to be paranoid about null checking. It’s clear where something might be null and the only time that an explicit check is required is when we really need to collapse the Option down to a single value. Most functions operating on the underlying value can be passed into the map() method, or its friends, and so never have to do null checking.
val x: Option[String] = None
val y: Option[Int] = x.map(_.length)
y match {
case None => println("(null) value")
case _ => println("This never happens")
}
val a: Option[String] = Some("some string")
val b: Option[Int] = a.map(_.length)
b match {
case None => println("This never happens")
case Some(length) => println(s"Original string length = $length")
}
When the Option does need to be collapsed into a value there are methods which allow for managing the two-sided nature of the type. Where a ‘default’ value can be provided a method such as orElse can be used. Where the default is challenging to generate a lazily evaluated getOrElse allows passing a method which will only be called when necessary.
Another way of thinking about Option[A] is as a collection which contains either 1 value or 0 values.
Yet another way of thinking about Option is as a monad, and it does not disappoint there having both a map(f: A => B) as well as a flatten() method.
The closed type hierarchy is characteristic of an algebraic data type and is a very useful technique in functional programming. In particular it works well with pattern matching.
Either
An Either[A, B] is a more general form of an Option[A], and an Option can also be represented as Either[None, A]. It represents a single value which might be of type A or of type B. Conventionally, the two types are accessed independently as the .left() and .right() members respectively, which return a LeftProjection[A] or RightProjection[B]. The two types are very like optional types and allow for independently mapping the values depending on what is contained within the Either[]. This is particularly useful when only one of the values needs to be transformed.
The Either type itself is modeled as an abstract data type with two subtypes – Left[A] and Right[B]. This gives the Either type the ability to engage in pattern matching.
Either is so close to Option that there are actually methods on the Scala implementations’ APIs which allow for flipping between the two – Either.asOption and Option.toLeft()and Option.toRight() allow for converting between the two types. Obviously, converting from an Either to an Option loses some information in the case of a Left.
In the functional world the convention of using Right to indicate success and a Left to indicate failure is very strong, with almost all uses of Either being used this way.
Try
Exceptions have been a part of procedural programming languages for a very long time. Originally, hardware was capable of signalling ‘abnormal’ conditions which should result in a ‘pause’ of normal operations in order to handle whatever caused the condition. That idea grew into the software exception handling we see today. In general, functional style languages attempt to signal all responses through the return value. Exceptions break this model by having an alternative return mechanism, creating complexity in the language and the caller. Unfortunately, much of the code in real-world libraries utilises exception throwing. The Try[T] type is designed to ease interaction with such code.
A Try[T] represents the result of a computation which may have returned an exception or a value of type T. In many ways a Try[T] is very similar to an Either[A,T]where A is an Exception. There are two subtypes of Try – a Success[T] and a Failure[T] allowing for effective pattern matching. It also allows for map-ing and flatMap-ing over its contents, collapsing to a value with a default in the exception case using orElse, or allowing exceptions to be handled using recover or recoverWith. A failure can also be mapped using the transform method. To help interact with other functional types there are also toOption and toEither methods.
With Try we can wrap calls to an external, third-party, exception-using library and then process the results functionally. This allows us to isolate exception handling to the edges of the application with core application processing not having to worry about exceptions at all.
Tuple
A tuple is just a loose collection of heterogeneous items. In Scala they look a lot like a sequence, but we can directly address them by ordinal. Unlike a sequence they have a fixed length with fixed types in each position. We generally use them in functional programming as a means of loosely coupling related items in an intermediate processing step. For example, if I have two sequences of integers and I want to find a sequence containing the pairwise sum of each one way of achieving that would be to zip the sequences together, producing a single sequence of tuples and then mapping over the sequence adding each tuple.
val list1: Seq[Int] = Seq(1, 2, 3) val list2: Seq[Int] = Seq(5, 6, 7) val intermediate: Seq[(Int, Int)] = list1.zip(list2) intermediate.map(_._1 + _._2)
Conclusion
All of the types presented here represent a value which is some generic form of composition of multiple types. These types are not unique to the functional world, but they have emerged from that world. By separating out null into a special case the Option type ensures it is handled appropriately when it needs to be and can be ignored when it doesn’t. More generically, the Either type allows any method to return one of two distinct types, and allows for generic error handling and presentation. The Try represents a specialisation of that mechanism tailored for interacting with library or legacy code which uses exceptions. Finally the Tuple allows a function to easily return multiple values without having to instantiate a distinct type to represent the combination. Where the combination occurs infrequently this is quick and easy. Later refactoring can then easily introduce a combination type if that proves valuable.
What we see from all this is how extensively the functional paradigm attempts to make use of types and the type system to signal complex, and sometimes disjoint, return values from functions. We bring return values into a single place, we never modify input parameters and we don’t complicate function signatures with exception specifications.