Search this site
Embedded Files
io.turbodsl
  • Home
    • Fundamentals
    • Default Mechanisms
    • Sync vs. Async Scopes
    • Asynchronous Results
    • Asynchronous Modes
    • Retry Pattern
    • Code Reusability
    • Collections
    • Performance Efficiency
io.turbodsl

Previous: Fundamentals

Next: Sync vs. Async Scopes

Default Mechanisms

Fail-Safe Patterns
Default parameters
Default values are optional
Default Constants

Fail-Safe Patterns

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")

}

Default parameters

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:

  1. default parameter is evaluted.
    If Default.Undefined is returned,  then...

  2. defaultFun lambda is evaluated.
    If Default.Undefined is returned, then...

  3. 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.

Default values are optional

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).

Default Constants

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()

    }) {

        :

    }

}

Previous: Fundamentals

Next: Sync vs. Async Scopes

Copyright 2024 © migueltt
Google Sites
Report abuse
Google Sites
Report abuse