I like Option.get
In strong, static, expressive FP languages, such as Scala, or Haskell, there’s the ongoing drive to “capture invariants in the type system” and “to make illegal states unrepresentable”. For a nice introduction, see Parse, don’t validate by Alexis King.
Option.get
or List.head
get such bad reps because these functions aren’t total. To wit:
val value: Option[String] = None
value.get
//=> java.util.NoSuchElementException: None.get
// at scala.None$.get(Option.scala:627)
This is indeed bad, because it fails at runtime. And if it fails at runtime, this means it can fail in production, in spite of all our unit tests and fancy CI setup.
In fairness, even with the current status quo, option.get
is still better than usage of null
, because developers are still aware that the value might be missing (by seeing the type, then having to call .get
, at the very least), and even in absence of such mindfulness, at least the exception is clearer, as NullPointerException
is often thrown due to faulty internals, and JVM initialization timing issues. At least you know it’s your own fault 🙂
If get
wasn’t available on Option
, then you’d be forced to do this:
value match {
case Some(v) => v
case None => "unknown"
}
// Or this ...
value.getOrElse("unknown")
And if you’d miss a case, you’d get a warning (or an error, if you work with fatal warnings):
value match { case Some(v) => v }
//=> warning: match may not be exhaustive.
// It would fail on the following input: None
The compiler can thus force you to deal with None
explicitly.
All of that aside however, in the following piece of code, what’s wrong isn’t the presence of Option.get
, but rather the compiler’s inability of seeing the if
expression:
// This is correct and will never trigger runtime exceptions
if (option.nonEmpty)
option.get // Like a boss 😎
else
"unknown"
In other words, I’ll blame Scala and Haskell, and not the availability of Option.get
. I learned to expect more from my tools. It’s not me, it’s you, Scala.
We could say that in absence of compiler features to cope with this, then .get
shouldn’t exist. However, programming languages are general purpose, and often get used in contexts in which strong static guarantees are not only useless, but get in the way. I still build my scripts in Ruby, because the static languages that I love are really bad for scripting. I’d like to disable some static guarantees, whenever brevity is important, and not correctness. E.g. for my own throwaway scripts I couldn’t care less that Option.get
throws exceptions.
TypeScript has untagged unions, and under --strict
this throws an error:
const sample: number | null = null
sample + 10
//=> error TS2531: Object is possibly 'null'.
// This works
if (sample !== null) {
sample + 10
}
Option
being boxed (tagged) provides us with benefits, like the ability to express Option<Option<A>>
(without auto-flattening), or the ability to define monadic operations for it. Here’s one way to express Option
in TypeScript:
type None = {
nonEmpty: false
}
type Some<A> = {
nonEmpty: true
value: A
}
type Option<A> = Some<A> | None
// ----------------------------------
const sample: Option<String> =
{ nonEmpty: false } // None
sample.value
//=> error TS2339: Property 'value' does not exist on type 'Option<String>'.
// Property 'value' does not exist on type 'None'.
// Compiles just fine
if (sample.nonEmpty) {
console.log( sample.value )
}
This is called Flow-sensitive typing. And minus some limitations and gotchas, it works just fine.
I hope Scala will evolve to do it too, because TypeScript, and Kotlin can already do this 🙂 and it would be a shame for Scala to not evolve such abilities, to go along with its brand new untagged union types.