Reduce Optional Argument Boilerplate
A common pattern in languages without support for optional types (e.g., Option
in Scala or Maybe
in Haskell) is to use a sentinel value to denote a method argument as being optional. If you were to define a method to read from a buffer, in Java say, with an optional argument to either read the entire buffer or a given number of bytes from the buffer you might end up with something like:
abstract class BufferReader {
/**
* Read the given number of bytes from the buffer.
*
* @param numBytes Read the given number of bytes from the buffer,
* or the entire buffer if the -1 is passed.
* @return Array of the bytes read from the buffer.
*/
public byte[] read(int numBytes);
}
In a language like Scala you might want to take advantage of the Option
type to improve the type safety of reading the entire buffer and you would end up with an interface similar to this:
trait BufferReader {
/**
* Read the given number of bytes from the buffer.
*
* @param numBytes Read the given number of bytes from the buffer,
* or the entire buffer if None.
* @return Array of the bytes read from the buffer.
*/
def read(numBytes: Option[Int]): Seq[Byte]
}
While this implementation improves on the type safety of the Java solution it results in method calls that always involve the boilerplate of wrapping values in Some
, read(Some(numBytes))
, when we'd ideally like to have an interface that takes an Option
when called for but allows us to call it in the more familiar Java style as well.
In Scala we can achieve this by taking advantage of implicit conversions resulting in methods that support both Option
arguments and the boilerplate free value arguments.
import scala.language.implicitConversions
trait OptionalArgument[T] {
def asOption: Option[T]
}
object OptionalArgument {
implicit def fromValue[T](value: T) = new OptionalArgument[T] {
def asOption = Some(value)
}
implicit def fromOption[T](value: Option[T]) = new OptionalArgument[T] {
def asOption = value
}
}
Given this infrastructure we can rewrite our earlier BufferReader
trait with it:
trait BufferReader {
/**
* Read the given number of bytes from the buffer.
*
* @param numBytes Read the given number of bytes from the buffer,
* or the entire buffer if None.
* @return Array of the bytes read from the buffer.
*/
def read(numBytes: OptionalArgument[Int] = None): Seq[Byte]
}
Now we can use a simple implementation to show how we retain the absence of a value through the type system while not losing any of the ergonomics we wanted from the Java solution. This simple abstraction allows you to call BufferReader.read(1)
or BufferReader.read(Some(1))
and to express the absense of a value with BufferReader.read(None)
. It keeps the calling API clean and easy to read without the unnecessary Option
boilerplate.
Built With: Scala 2.11.7