Is using Optional in Scala's case classes and classes fields a code smell?
Short answer
Option
fields have use cases; they're not intrinsically bad. However, even though several well established libraries (e.g. ScalaTest) define classes with Option
fields, the latter, IMO, tend to be a code smell, as they often try to do too much for their own good.
In many cases, a type containing optional fields can easily and advantageously be replaced by an algebraic data type.
An example
The domain
Consider a business domain dealing with accounts. An account starts its life one day as an open account, but may eventually be closed. Accounts, among other data, contains the dates on which they were open and closed, where applicable.
Using an Option
field
Here is an implementation of an account, using an Option
field:
final case class Account(openedOn: LocalDate, closedOn: Option[LocalDate], ...)
We also have an account service, which defines, among other things, a close
method:
trait AccountService {
// ...
def close(account: Account): Account
}
This approach is problematic, for a number of reasons. One problem is that Account
isn't particularly performant: because closedOn
is a "boxed" type, you have one level of indirection too many, so to speak. Moreover, Account
's memory footprint is less than ideal: a "closed account" contains a pretty uninteresting value (None
), which is a waste of space.
Another, more serious, problem is that the close
method cannot enforce, at the type level, that the parameter be an "open account" and the result be a "closed account". You would have to write tests to check that this business rule is enforced by your implementation.
Using a small ADT (and eschewing Option
fields)
Consider the following alternative design:
sealed trait Account { ... }
final case class OpenAccount(openedOn: LocalDate, ...) extends Account
final case class ClosedAccount(openedOn: LocalDate, closedOn: LocalDate, ...) extends Account
This small ADT remedies the performance problem, but there is more... You can now encode the business rule at the type level! This is an example of making illegal states unrepresentable (a phrase attributed to Yaron Minsky). As a result, your service's API becomes more expressive and harder to misuse:
trait AccountService {
// ...
def close(account: OpenAccount): ClosedAccount
}
This example may be sufficient to convince you that the second approach is preferable, and that Option
fields are best avoided (or, at least, used sparingly).
Resources
For more more about eliminating optional fields towards making illegal states unrepresentable, see
- Yaron Minsky's blogpost
- Scott Wlaschin's blogpost
- Richard Feldman's elm-conf 2016 talk (skip to the 21'25'' mark, then rewind and watch the whole talk for great good!)
Option of scala implements Serializable
Usage of Option
in scala is highly recommended for nullable attributes. Option[T]
is considered better than T
because the former is more typesafe than than latter.
As of now, using Optional for class members in Java is widely recognized as a code smell
on the contrary presence of null
in-place of Optional attribute in scala is considered a code-smell.
As much as Scala is a functional language it also a language that promotes type-safety. In an Ideal world a truly fully typesafe language will not have runtime exceptions like NullpointerException
and Option
plays a important role in Scala to avoid it.
The Option[T] explicits states that the attribute can be in the state of null (i.e. None
) and forces the clients of the attribute to handle the null
scenario. Thus Option adds more information to the type system and makes the code more typesafe.
With language feature such as such as pattern matching and Monad/Monoid the economics of using Optional datatypes in Scala is very cheap and user friendly in Scala compared to Java.
Pattern matching:
optionalVariable match {
case Some(x) => /* handle when variable has a value*/
case None => /* handle when the variable doesn't have a value*/
}
Option as Monad:
optionalVariable foreach { x => /* run code when the variable is defined*/ }
optionalVariable map { x => /* map optional type to another type */}
Edit:
Jubobs makes very good case where using Option can be replaced with custom types. But I think there are many more cases where Optional attributes make more sense. For eg: If the Account object has optional attributes such as emailId
and phoneNo
then Option[T] would be a better solution since creating custom types for each combination would be impractical and would lead to class explosion.