« Primitive Conclude types | Stardust 6 | Immutable Collections »
Key Features of Stardust 6
Fail-Safety with Results and Enclosed Environments
What’s Wrong with Exceptions?
Exceptions are basically a nice thing: if something fails, throw an exception and let it handle someone else—out of sight, out of mind. And exactly this is a major drawback of exceptions. Additionally, a function call does not provide any information about possibly thrown exceptions; only the documentation may contain information about this—but only if the author was conscientious. This causes a lot of errors where exceptions are not caught properly.
// May throw an exception, but was not documented
fun addValues(a: Int, b: Int, c: Int): Int =
    Math.addExact(Math.addExact(a, b), c)
Wouldn't it be great, if we could document possible exceptions within the source code? But wait, there is a feature that does something like this: Java’s checked exceptions. The idea of checked exceptions was good, but nowadays, when using features like lambda expressions, they’re rather hindering than helpful. That’s one reason, why Kotlin has no checked exceptions, but Kotlin does not provide an alternative way to document possible exceptions than in the documentation comment.
But Kotlin is such a powerful programming language—can’t we use some of its features to realize better exceptions? Yes, with a combination of results and context parameters, we can do this!
Using Results
The way to document exceptions in the source code is to use results (in Stardust, a result is of the Conclude type).
If a function returns a result, it unmistakably makes clear, that it may return an error (in Stardust: Failure).
To get the computed value out of the result, it has to be handled accordingly.
This forces you to deal with possible errors.
As a pendant to Math.addExact, Stardust provides the plusExact function …
infix fun Int.plusExact(other: Int): ConcludeInt<ArithmeticException>
… that returns a result instead of throwing an exception:
val result1: ConcludeInt<ArithmeticException> = 27 plusExact 15
val result2: ConcludeInt<ArithmeticException> = 27 plusExact Int.MAX_VALUE
println("27 + 15  = $result1")
println("27 + MAX = $result2")
27 + 15  = SuccessInt(value=42)
27 + MAX = FailureInt(reason=java.lang.ArithmeticException: integer overflow)
Introducing “Enclosed Environments”
For single calls, returning results to enforce error handling is a good solution.
But functions like plusExact are mostly called in a chain.
This would require to handle intermediate results on each call,
which would lead to bloated code:
val result: ConcludeInt<ArithmeticException> = (16 plusExact 11).flatMap { it plusExact 15 }
println(result)
SuccessInt(value=42)
How can we do better?
To circumvent the awkward handling of intermediate results, Stardust introduces so-called “enclosed environments” that encapsulate functions that may throw exceptions by returning a result. There is a generic enclosed environment available for reference types and also additional implementations for all primitive types:
// Genric enclose for reference types:
inline fun <Result, reified Error: Exception> enclose(block: EnclosingContext<Error>.() -> Result): Conclude<Result, Error>
// Primitive enclose, here for the `Int` type:
inline fun <reified Error: Exception> encloseInt(block: EnclosingContext<Error>.() -> Int): ConcludeInt<Error>
To make use of an enclosed environment, Stardust provides a special overload of plusExact that is bound on a context parameter:
context(_: EnclosingContext<ArithmeticException>)
infix fun Int.plusExact(other: Int): Int
This allows us to use plusExact as a normal function within an enclosed environment that returns an Int instead of a result .
The enclosed environment encloseInt returns a result (ConcludeInt<ArithmeticException>) that contains a Failure if plusExact has detected an overflow.
This way, we can chain those function calls comfortably and handle the result afterwards.
val result: ConcludeInt<ArithmeticException> = encloseInt { 16 plusExact 11 plusExact 15 }
println(result)
SuccessInt(value=42)
That means, by default, the plusExact function returns a result, but in an enclosed environment, it returns the concrete type.
The compiler automatically selects the correct overload.
This way, we cannot miss handling the exception.
Isn’t this awsome?!
Enclosed environments are used in many places in Stardust, not only for adding integers.
For example, Stardust's date and time API (stardustx-datetime module) makes heavy use of it.
Throwing Anyway with “Throw Environments”
In some cases, you may want to throw an exception for certain reasons—like panicking in other programming languages.
To do so, you can either call Conclude.orThrow() on the result of the enclosed environment,
or you can use the special throw environments:
// Genric throw environment for reference types:
inline fun <Result, reified Error: Exception> throws(block: EnclosingContext<Error>.() -> Result): Result
// Primitive throw environment, here for the `Int` type:
inline fun <reified Error: Exception> throwsInt(block: EnclosingContext<Error>.() -> Int): Int
In bose cases, you’re enforced to document directly in the source code that the instruction may panic:
// Throw on failure using `ConcludeInt.orThrow`:
val result1: Int = encloseInt { 16 plusExact 11 plusExact someValue }.orThrow()
// Throw directly using a throw environment:
val result2: Int = throwsInt { 16 plusExact 11 plusExact someValue }
« Primitive Conclude types | Stardust 6 | Immutable Collections »