There are some communities where this or similar things are fairly common. For example, in C, many functions that can normally only return positive integers will return an error code as a negative integer in case of failure. For example, printf
returns the total number of characters written (which can never be negative, obviously). However, its return type is not typed as unsigned int
but as int
, and it returns a negative number in case of failure.
I am pretty sure I have seen the exact thing you describe done in some dynamic language, but I can't come up with an example right now.
However, there are almost always better alternatives.
If your language has something like conditions (CommonLisp, Dylan, Clojure, …) or exceptions (ECMAScript, PHP, Python, Ruby, …), then you can use those.
If your language supports multiple return values, then you can use an extra return value for the error. For example, in Go, it is idiomatic to return the error as an extra return value:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}
And the caller would use it like this:
n, err := Sqrt(-1)
if err != nil {
log.Fatal(err)
}
// Do something with `n`
In languages with lightweight syntax for tuples or records, and destructuring assignment, you can easily "fake" support for multiple return values (e.g. TypeScript):
function sqrt(n: number): { result?: number, error?: string } {
if (n < 0) {
return { error: `Argument must be non-negative but you passed ${n}!` };
}
// implementation
}
const { result, error } = sqrt(-1);
if (error) {
// handle error
}
In more verbose languages like Java, you can still build a simple result type, although it starts to get a little wordy:
record DoubleResult(double result, Throwable error);
DoubleResult sqrt(double d) {
if (d < 0d) {
return new DoubleResult(0d, new ArgumentException("Bla bla"));
}
// implementation
}
var dr = sqrt(-1d);
if (dr.error != null) {
// handle error
}
However, my personal favorite is the following: If you are using a statically typed language with parametric polymorphism, you can build an Error
type, something like this:
sealed trait Try[+T] {
val isSuccess: Boolean
val isFailure: Boolean
// @returns the result value if successful or the default value if failed
def getOrElse[U >: T](default: => U): U
// execute `f` with the result if success
def foreach[U](f: T => U): Unit
// transform result using `f` if success
def map[U](f: T => U): Try[U]
// transform result using `f` if failure
def recover[U >: T](f: Throwable => U): Try[U]
}
final case class Failure[+T](exception: Throwable) extends Try[T] {
override val isSuccess = false
override val isFailure = true
override def getOrElse[U >: T](default: => U) = default
override def foreach[U](f: T => U) = ()
override def map[U](f: T => U) = this.asInstanceOf[Failure[U]]
override def recover[U >: T](f: Throwable => U) = Success(f(exception))
}
final case class Success[+T](value: T) extends Try[T] {
override val isSuccess = true
override val isFailure = false
override def getOrElse[U >: T](default: => U) = value
override def foreach[U](f: T => U) = f(value)
override def map[U](f: T => U) = Success[U](f(value))
override def recover[U >: T](f: Throwable => U) = this
}
This is an Error
type which you might typically find in a functional language, but there is nothing inherently tied to functional programming about it. In fact, the sketch above is completely OO, using subtyping and method overriding.
The idea behind this type is that it has an abstract superclass that defines operations both for processing the result if it exists, and for handling the failure if it happens. Then, it has two concrete subclasses, one for the successful case, and one for the failure case.
And we are using simple method overriding, so that the Success
class overrides or implements all the failure handling methods as no-ops, and the Failure
class handles all the success handling methods as no-ops.
The real trick though, is that this class implements also a collection-style interface: it has foreach
and map
, etc. In the success case, it behaves like a collection with one element, and in the failure case, it behaves like an empty collection.
So, in many cases, I don't even need to check whether it was successful or not! If I want to transform the result, I can simply call map
, because map
on an empty collection is a no-op.
We can implement additional collection methods, I just didn't show them in the sketch. E.g. flatten
, which will flatten a nested tower of Try
s into a single level of Try
which is either the last Error
if there was at least one, or a Success
containing the value. Similarly, we can implement flatMap
.
I can pass this object on to a different method, and that method can simply call map
as well. Only the method that actually wants to handle the error and get rid of the Try
wrapper around the value needs to actually know about the error.
The fact that this is essentially a collection, means that all the things that we know about collections, all the methods in the standard library that deal with collections, can be applied to this. We can simply transparently treat it as a collection and it will automatically "do the right thing". We only need to care about the error at the place where we actually care about the error.
I have avoided the "M" word for now, but you might have already spotted that this is a monad (well, not quite, I left out the flatMap
method in the sketch). Several languages nowadays have special syntax sugar for monadic operations, e.g. C#, Visual Basic.NET, Scala, and Haskell. In those languages, dealing with Errors using an Error Monad is very simple.
Such an error type is nowadays built into the standard libraries of many languages. For example, Scala has scala.util.Try
, Rust has std::result::Result
, Elm has Result
, and so on.
These work best in languages that have monad comprehensions and pattern matching, but those features are not necessary. In some cases (e.g. Rust) there is special syntax for dealing with result types.