Handling errors is usually implemented as:
Use try-catch statements / expressions.
Log error / exception.
Return a default value, or re-throw error / exception
This becomes even more critical when you are using external libraries where you do not have visibility on the internal implementation, or, if the related code is quite extensive.
io.turbodsl simplifies this common pattern by including optional parameters to specify such default value.
// Traditional approach
fun doSomething(): String =
try {
// operations to perform
:
} catch (e: Throwable) {
// log `e`, and return a default value
:
}
// io.turbodsl approach
fun doSomething(): String =
TurboScope.execute<String>(default = <...>) {
// operations to perform
:
}
TurboScope.execute {
val tmp = job<String>(
default = Default.Defined("Foo")
) {
:
}
// tmp = "Foo"
}
// Main entry point for execution
fun main() {
val tmp = TurboScope.execute<String>(
defaultFun = {
// "this" refers to `TurboScope
"Bar".toDefault()
}
) {
// "this" refers to `TurboScope
throw Error()
}
// tmp is "Bar"
}
// Anywhere within your codebase
TurboScope.execute {
// "this" refers to `TurboScope`
val tmp = job<Foobar>(
defaultFun = {
// "this" refers to `JobScope`
Foobar("foo").toDefault()
}
) {
// "this" refers to `JobScope`
throw Error()
}
// tmp is Foobar("foo")
}
Two different parameters have been added to all scopes:
default → value to return if any exception / error is thrown within the scope.
Must be a Default.Defined. You can use extension function <Any?>.toDefault().
This approach should be used whenever the default value is a simple data structure.
defaultFun → a lambda that returns the default value.
This lambda is executed, if and only if, the scope throws an exception / error
It must be fail-safe. Any error will be re-thrown as a ScopeImplementationException.
This should be used whenever the default value evaluation is expensive:
Default value is a complex data structure containing multiple attributes with different types.
Default value depends on complex statements / expressions.
The lambda is executed within the same scope, allowing to use any io.turbodsl expressions.
If you specify both parameters:
default parameter is evaluted.
If Default.Undefined is returned, then...
defaultFun lambda is evaluated.
If Default.Undefined is returned, then...
the scope exception / error is thrown.
IMPORTANT: Whenever you call a function, the expressions for each parameter are evaluated. For arguments that are lambdas, these are not executed, but passed as "function" instances.
Therefore, the actual lambda execution is deferred.
Traditionally, null is used whenever a variable may not contain a value. This creates a problem since null could potentially represent a valid value in some scenarios. This is even more relevant when using generics.
For example:
fun <T, R> test(input: T, block: (T) -> R): R = block.invoke(input)
In this case, test will receive a T value and execute block, returning R.
This could potentially be used as follows:
val foo: String? = test<Int, String?>(input = 10) {
// perform some calculation returning a `String?`
}
val bar: Boolean? = test<String?, Boolean?>(input = foo) {
when {
it.isNullOrBlank() -> null
it.length > 10 -> true
else -> false
}
}
The nullity for parameter input in function test is defined by the caller's. The same applies for the return value.
Using null as a mechanism to detect whether an argument was specified is not correct since generics T and R could potentially be defined as nullable (?).
io.turbodsl uses generics extensively, including the definition for default values, which are optional. That's the main reason of having a special class hierarchy to identify default values:
sealed class Default<out T> {
data object Undefined : Default<Nothing>()
class Defined<out T>(val value: T) : Default<T>()
}
default parameter is set to Default.Undefined, unless you specify a Default.Defined.
defaultFun parameter is set to null, unless you specify a valid lambda returning a Default (either Defined or Undefined).
For simple scenarios, Default.Defined instances can be specified.
The following constants can be used for common values:
Default.Unit → equivalent to Unit
Default.True → equivalent to true
Default.False → equivalent to false
Default.EmptyString → equivalent to ""
Default.Zero → equivalent to 0
Default.One → equivalent to 1
Default.MinusOne → equivalent to -1
Default.Undefined → No default value - this is the internal default
TurboScope.execute {
job(default = Default.Unit) { ... }
job(default = Default.True) { ... }
job(default = Default.False) { ... }
job(default = Default.EmptyString) { ... }
job(default = Default.Zero) { ... }
job(default = Default.One) { ... }
job(default = Default.MinusOne) { ... }
job(default = if (true) Default.False else Default.Undefined) { ... }
job<Boolean>(defaultFun = { Default.True }) { ... }
job<Boolean>(defaultFun = { Default.False }) { ... }
job<Boolean>(defaultFun = {
val condition: Boolean = async(
job1 = asyncJob<Foobar> { ... },
job2 = asyncJob<String> { ... },
) { ok, r1, r2 ->
// calculate condition based on r1 and r2
:
}
// If the condition is true, then there's a default.
// Otherwise, let the main block fail.
if (condition) Default.False else Default.Undefined
}
) { ... }
}
For null default values:
You must specify the return type as nullable (?).
You must use <Any?>.toDefault().
This will actually use private constant Default.Null.
TurboScope.execute {
job<Foobar?>(default = null.toDefault()) {...}
job<Foobar?>(defaultFun = {
job<Foobar?> {
// Retrieve some Foo value using some default mechanisms.
// If nothing was found, returns null.
null
}.toDefault()
}) {
:
}
}