Skip to content

Instantly share code, notes, and snippets.

@davidallsopp
Last active August 29, 2015 14:05
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save davidallsopp/ffcebb9ff3c031bf9b8b to your computer and use it in GitHub Desktop.
An explanation of using the Option type in Scala (or the Maybe type in Haskell) rather than null. See also http://speaking-my-language.blogspot.co.uk/2010/08/why-scalas-option-wont-save-you-from.html for futher discussion/argument, and https://gist.github.com/davidallsopp/79fd49840197f3597b72 for an example of representing an object lifecycle in…
// An explanation of using the Option type in Scala rather than null (in languages such as Java)
// The equivalent in Haskell is the Maybe type
// The Option type is for values that might or might not exist - i.e. exactly the use case
// that null fulfills. It has two subtypes: Some[A] and None
// ( where A is a type parameter - i.e. Some is a generic type. )
val yes = Some("Hello world!")
val no = None
// This prevents NullPointExceptions because we no longer deal with the actual reference;
// it is safely wrapped in the Some, or completely absent in the case of None.
// We cannot call methods of the enclosed type (String, in this example) on a None, because
// it doesn't have those methods!
no.length() // Does not compile
// Nor can we directly call those methods on a Some[String]:
yes.length() // Still does not compile
// So how can we do anything useful? We pass functions to the Option to be executed safely,
// depending on whether we have a Some or a None.
no.foreach(println) // Does nothing
yes.foreach(println) // prints "Hello world!"
no.map(msg => msg + " Goodbye.") // returns None
yes.map(msg => msg + " Goodbye.") // returns Some("Hello World! Goodbye.")
// Note that these return another Option, so that any following operations
// are also performed safely, and can be conveniently chained together:
yes.map(msg => msg + "!!!").map(_.length) // Some(15)
no.map(msg => msg + "!!!").map(_.length) // None
// If we were going to return a value from a function, rather than print something out,
// then our function can return an Option too, which clearly indicates the missing
// value to the caller rather than ignoring it:
def doSomething(opt: Option[String]) = opt.map(usefulFunction) // returns None if opt was None
// Or we could provide different behaviours for the two cases:
opt match {
case Some(a) => doSomething(a)
case None => println("No value")
}
// Note that if I only provided the case Some(a) in the "match" statement above, then
// the compiler will complain that I haven't covered the None case. So I cannot forget the "null check"
// Alternatively, we can provide a default, so we are guaranteed to get a non-null value:
val n: String = no.getOrElse("Shhh") // "Shhh"
val y: String = yes.getOrElse("Shhh") // "Hello World!"
// Or we could resort to runtime exceptions (if you insist):
// If we want an immediate runtime exception for missing values, we can do
println(no.get())
// Or assert something:
assert(no.isEmpty, "Oops!")
assert(yes.isDefined, "Oops!")
// Or we could be more explicit, and cover both cases:
opt match {
case Some(a) => doSomething(a)
case None => throw new Exception("Missing value")
}
// but this is as verbose as a traditional null check, so should generally be avoided.
// Also, we should not be using Options (or nulls) unless values really are optional.
// If they are required, then they should be a non-nullable (ideally immutable) value
// If they are required to be absent, then don't represent them at all!
// If there is a lifecycle, so the requirement changes, then represent this explicitly:
// See https://gist.github.com/davidallsopp/79fd49840197f3597b72
// Options really start to shine for more complex (realistic) cases, where we have nested null checks.
// Quick - did we remember all the null checks in the following code??
// Java-style, but converted to Scala
def listLocations(start: Int, end: Int) {
for (id <- start to end) {
val user = db.getUser(id)
if (user != null) {
val address = user.getAddress()
if (address != null) {
val location = address.getLocation()
if (location != null) {
val lat = location.getLat()
val lon = location.getLon()
if (lat != null && lon != null) {
println("Location: " + lat + ", " + lon)
}
}
}
}
}
}
// Using Option, the 'null checks' are performed automatically under the hood of the for-comprehension:
def listLocations(start: Int, end: Int) {
for {
id <- start to end
user <- db.getUser(id)
address <- user.getAddress
location <- address.getLocation
(lat, lon) <- location.getLatLon
} println(s"Location: ${lat}, ${lon}")
}
// Note: if these operations return error messages that you want to capture rather than ignore,
// then you can use the Either type instead of Option. If they throw exceptions that you want to
// handle, use the Try type instead.
// In Scala we can also use Option.get() to extract the value, if it exists
// (i.e. if the Option is a Some). The get() method is widely regarded as a mistake. Haskell
// does not have such a mechanism. However, get() immediately throws a runtime exception if
// there is no value, so a null does not propagate to cause mysterious damage elsewhere.
// Note also that although Scala does also support null (for compatibility with Java), languages
// like Haskell simply don't have null - you just use the Maybe type.
// The examples above are just a taste - there is much more that these types can do that makes them
// far more convenient than null-checks, as well as safer - e.g. handling lists of Options.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment