App

Overview

Result is a foundational component in Slate Kit for accurately modeling successes and failures using a functional approach to error handling. Result is not a new concept as it currently exists in various forms in other languages ( see below). In fact, the Result type in Slate Kit has been inspired by these and also by Google’s GRPC codes ( See below ). The implementation here differs mainly with the design/goals listed below.

Goals



Design Description
1. Accuracy Accurate modeling of success/failures
2. Flexible error Error type on the Failure branch can be anything, Exception, Err, String.
3. Status Codes Logical groups of status codes to categories errors, which can be converted to Http
4. Sensible defaults Default Error types, and builders are provided to reduce custom errors / boiler-plate


Compare

# Language Name Notes
1 Kotlin Result Error type is Exception and currently for internal use
2 Swift Result Very similar conceptually
3 Rust Result Branches are Ok / Err rather than Success / Failure
4 Scala Either General purpose use to represent A or B, branches are Left / Right
5 Scala Try Models successes and failures with exceptions
6 Go GRPC Codes Status codes in Slate Kit are inspired by these


Status

This component is currently stable, has 0 dependencies and can be used for both Android and Server
Back to top



Install

Use the following settings in gradle for installing this component.

    repositories {
        // other repositories
        maven { url  "http://dl.bintray.com/codehelixinc/slatekit" }
    }

    dependencies {
        // other dependencies ...

        compile 'com.slatekit:slatekit-result:1.0.0'
    }
package slatekit.result
jar slatekit.result.jar
git slatekit/src/lib/kotlin/slatekit-result
docs App
uses n/a
license Apache 2.0
example Example_Results.kt

Back to top



Requires

This component has 0 dependencies and does NOT use any other Slate Kit components.
Back to top



Sample

Here are some examples showing the usage of Result by creating, checking, and pattern matching the values. Refer to the Example_Result.kt

       
    import slatekit.results.*

    // Create success explicitly
    val start:Result<Int,Err> = Success(10 )

    // Properties
    println(start.success)     // true
    println(start.status.code) // Codes.SUCCESS.code
    println(start.status.msg)  // Codes.SUCCESS.msg

    // Safely operate on values with map/flatMap
    val addResult = start.map { it + 1 }
    val subResult = start.flatMap { Success(it - 1 ) }

    // Check values
    println( addResult.contains(11) )
    println( addResult.exists{ it == 11 } )

    // Get values
    println( addResult.getOrNull() )
    println( addResult.getOrElse { 0 })

    // On conditions
    subResult.onSuccess { println(it) } // 9
    subResult.onFailure { println(it) } // N/A

    // Pattern match on branches ( Success / Failure )
    when(addResult) {
        is Success -> println("Value is : ${addResult.value}") // 11
        is Failure -> println("Error is : ${addResult.error}") // N/A
    }

    // Pattern match on status
    when(addResult.status) {
        is Status.Succeeded  -> println(addResult.msg)
        is Status.Pending    -> println(addResult.msg)
        is Status.Denied     -> println(addResult.msg)
        is Status.Invalid    -> println(addResult.msg)
        is Status.Ignored    -> println(addResult.msg)
        is Status.Errored    -> println(addResult.msg)
        is Status.Unexpected -> println(addResult.msg)
    }
        

Back to top



Concepts

These are the main concepts / terms to know for using this component. All of these are explained further below in the details section. You can find links to the source code here.


Concept Usage Description
Result required Main type for modeling successes and failures.
Success required Success branch/sub-class of Result storing the value with optional status code
Failure required Failure branch/sub-class of Result storing the error with optional status code.
Err Optional Flexible representation of errors. E.g. You can use the Err interface, default implementations or Exceptions
Status Optional Logical error groups to reduce boiler plate and offer HTTP compatibility. E.g. Succeeded, Denied, Invalid, etc
Codes Optional Default implementations of general purpose status codes
Aliases Optional Type aliases to simplify the Result from 2 type parameters to 1. E.g. Outcome, Try, Notice, Validated
Builders Optional Convenient methods to build errors such as Outcomes.denied

Back to top



Guide


Name Description More
1. Create Create successes, failures in various ways more
2. Get Get values safely using available operations more
3. Check Check for values or use pattern matching more
4. Errors Define your error types or use pre-built ones more
5. Status Use status codes for logical grouping more
6. Aliases Type aliases to default the error type more
7. Builders Several easy ways to build success or different category of errors more
8. Outcome A type alias for Result[T, Err] where Err is an error interface more
9. Try A type alias for Result[T, Exception] to work with Exceptions more
10. HTTP Support Convert Successes / Failures to compatible HTTP codes more

Back to top



Creation

There various ways to create successes / failures, and these will be similar to Kotlin / Swift Result type usage.

      
    import slatekit.results.*
    
    // Success: Straight-forward
    val result = Success(42)

    // Success referenced as base type Result<Int, Err>
    val result1a: Result<Int, Err> = Success(42)

    // Success created with status codes / messages
    val result1b = Success(42, status = Codes.SUCCESS)
    val result1c = Success(42, msg = "Successfully processed")
    val result1d = Success(42, msg = "Successfully processed", code = 200)

    // Failure
    val result1e = Failure(Err.of("Invalid email"))

    // Failure referenced as base type Result<Int, Err>
    val result1f: Result<Int, Err> = Failure(Err.of("Invalid email"))

    // Failure created with status codes / messages
    val result1g = Failure(Err.of("Invalid email"), status = Codes.INVALID)
    val result1h = Failure(Err.of("Invalid email"), msg = "Invalid inputs")
    val result1i = Failure(Err.of("Invalid email"), msg = "Invalid inputs", code = Codes.INVALID.code)
     
Back to features Back to top


Get

Result offers the typical functional ways to safely get the value

      
    import slatekit.results.*

    // Create
    val result:Result<Int, Err> = Success(42)

    // Get value or default to null
    val value1:Int? = result.getOrNull()

    // Get value or default with value provided
    val value2:Int = result.getOrElse { 0 }

    // Map over the value
    val op1 = result.map { it + 1 }

    // Flat Map over the value
    val op2 = result.flatMap { Success(it + 1 ) }

    // Fold to transform both the success / failure into something else ( e.g. string here )
    val value3:String = result.fold({ "Succeeded : $it" }, {err -> "Failed : ${err.msg}" })

    // Get value if success
    result.onSuccess { println("Number = $it") }

    // Get error if failure
    result.onFailure { println("Error is ${it.msg}") }

    // Pattern match
    when(result) {
        is Success -> println(result.value)  // 42
        is Failure -> println(result.error)  // Err
    }
Back to features Back to top


Check

There are 3 ways to check / pattern match the result type and its actual branch/values. These range from coarse to fine grained matches depending on the situation. Typically though, as with other implementations you just need to check for Success / Failure. However, you can also check the categories of errors or the specific error types.

     
    import slatekit.results.*

    val result:Result<Int,Err> = Success(42)
    
    // Check if the value matches the criteria
    result.exists { it == 42 } // true
    
    // Check if the value matches the one provided
    result.contains(2)        // false

    // Pattern match scenario 1: "Top-Level" on Success/Failure (Binary true / false )
    when(result) {
        is Success -> println(result.value)  // 42
        is Failure -> println(result.error)  // Err
    }

    // Pattern match scenario 2: "Mid-level" on Status ( 7 logical groups )
    // NOTE: The status property is available on both the Success/Failure branches
    when(result.status) {
        is Status.Succeeded  -> println(result.msg) // Success!
        is Status.Pending    -> println(result.msg) // Success, but in progress
        is Status.Denied     -> println(result.msg) // Security related 
        is Status.Invalid    -> println(result.msg) // Bad inputs / data
        is Status.Ignored    -> println(result.msg) // Ignored for processing
        is Status.Errored    -> println(result.msg) // Expected errors
        is Status.Unexpected -> println(result.msg) // Unexpected errors
    }

    // Pattern match scenario 3: "Low-Level" on numeric code
    when(result.status.code) {
        Codes.SUCCESS.code    -> "OK"
        Codes.QUEUED.code     -> "Pending"
        Codes.UPDATED.code    -> "User updated"
        Codes.DENIED.code     -> "Log in again"
        Codes.DEPRECATED.code -> "No longer supported"
        Codes.CONFLICT.code   -> "Email already exists"
        else                  -> "Other!!"
    }
     
Back to features Back to top


Errors

Unlike Kotlin / Swift Result error types, the Err type on the Failure branch can be anything. It is not defaulted to Exception. In this way, this is conceptually similar to an Scala / Haskell Either.

    
    import slatekit.results.Err
    import slatekit.results.ErrorList

    // Build Err using convenience methods
    // Simple string
    val err1 = Err.of("Invalid email")

    // Exception
    val err2 = Err.ex(Exception("Invalid email"))

    // Field: name / value
    val err3 = Err.on("email", "abc123@", "Invalid email")

    // String message from status code
    val err4 = Err.code(Codes.INVALID)

    // List of error strings
    val err5 = Err.list(listOf(
        "username must be at least 8 chars", 
        "username must have 1 UPPERCASE letter"), 
    "Username is invalid")

    // List of Err types
    val err6 = ErrorList(listOf(
            Err.of("email", "abc123 is not a valid email", "Invalid email"),
            Err.of("phone", "123-456-789 is not a valid U.S. phone", "Invalid phone")
    ), "Please correct the errors")

    // Create the Failure branch from the errors
    val result:Result<UUID, Err> = Failure(err6)
     
Back to features Back to top


Status

The distinguishing feature of Slate Kit Result is the introduction of a Status Code component on the Result type. The status is applicable for both the Success / Failure branch. There are also default codes for most common errors. Codes. The Status Codes provide a few main benefits:

1. Categorization Categorize errors into logical groups. E.g. Succeeded, Invalid, Denied, etc.
2. Reduce boiler-plate Removes boiler-plate code of creating small custom error types. E.g. Outcomes.denied(“No access to resource”)
3. Conversion Provides a reasonable way to conver to HTTP codes E.g. Succeeded -> HTTP 200 range
    package slatekit.results

    sealed class Status {
        abstract val code: Int
        abstract val msg: String
    
        data class Succeeded(override val code: Int, override val msg: String) : Status()
        data class Pending  (override val code: Int, override val msg: String) : Status()
        data class Denied   (override val code: Int, override val msg: String) : Status()
        data class Ignored  (override val code: Int, override val msg: String) : Status()
        data class Invalid  (override val code: Int, override val msg: String) : Status()
        data class Errored  (override val code: Int, override val msg: String) : Status()
        data class Unexpected(override val code: Int, override val msg: String) : Status()
    }

Back to features Back to top


Aliases

There are a few type Aliases for making it easier to work with error types and to simplify the Result type from 2 type parameters down to 1. The aliases are supplemented with builder functions to easily create the result types.

    import slatekit.results.* 
    import slatekit.results.builders.Outcomes
    import slatekit.results.builders.Tries
    
    // Try<T> = Result<T, Exception>
    val res1 = Tries.attempt { "1".toInt() }

    // Outcome<T> = Result<T, Err>
    val res2 = Outcomes.of { "1".toInt() }

    // Notice<T> = Result<T, String>
    val res3 = Notices.notice { "1".toInt() }

    // Validated<T> = Result<T, ErrorList>
    val res4:Validated<String> = Failure(ErrorList(listOf(
            Err.of("email", "abc123 is not a valid email", "Invalid email"),
            Err.of("phone", "123-456-789 is not a valid U.S. phone", "Invalid phone")
    ), "Please correct the errors"))
      
Back to features Back to top


Builders

Typically, errors fall into various categories, and you may want to use specific messages, or other data to build up the errors. In order to facilitate these there are Builders available for convenience to construct appropriate error types.

    ...
    fun <T> denied(): Result<T, E> = Failure(errorFromStr(null, Codes.DENIED), Codes.DENIED)
    fun <T> denied(msg: String): Result<T, E> = Failure(errorFromStr(msg, Codes.DENIED), Codes.DENIED)
    fun <T> denied(ex: Exception): Result<T, E> = Failure(errorFromEx(ex, Codes.DENIED), Codes.DENIED)
    fun <T> denied(err: Err): Result<T, E> = Failure(errorFromErr(err, Codes.DENIED), Codes.DENIED)

    fun <T> ignored(): Result<T, E> = Failure(errorFromStr(null, Codes.IGNORED), Codes.IGNORED)
    fun <T> ignored(msg: String): Result<T, E> = Failure(errorFromStr(msg, Codes.IGNORED), Codes.IGNORED)
    fun <T> ignored(ex: Exception): Result<T, E> = Failure(errorFromEx(ex, Codes.IGNORED), Codes.IGNORED)
    fun <T> ignored(err: Err): Result<T, E> = Failure(errorFromErr(err, Codes.IGNORED), Codes.IGNORED)

    fun <T> invalid(): Result<T, E> = Failure(errorFromStr(null, Codes.INVALID), Codes.INVALID)
    fun <T> invalid(msg: String): Result<T, E> = Failure(errorFromStr(msg, Codes.INVALID), Codes.INVALID)
    fun <T> invalid(ex: Exception): Result<T, E> = Failure(errorFromEx(ex, Codes.INVALID), Codes.INVALID)
    fun <T> invalid(err: Err): Result<T, E> = Failure(errorFromErr(err, Codes.INVALID), Codes.INVALID)

    ...
Back to features Back to top


Outcome

The Outcome is simply a type alias for Result[T, Err] and allows you to use Result as Outcome[T]. This also comes with a Outcomes builder to construct Successes / Failures easily.

    import slatekit.results.* 
    import slatekit.results.builders.Outcomes

    // Outcome<Int> = Result<Int, Err>
    val res1 = Outcomes.success(1, "Created User with id 1")
    val res2 = Outcomes.denied<Int>("Not authorized to send alerts")
    val res3 = Outcomes.ignored<Int>("Not a beta tester")
    val res4 = Outcomes.invalid<Int>("Email is invalid")
    val res5 = Outcomes.conflict<Int>("Duplicate email found")
    val res6 = Outcomes.errored<Int>("Phone is invalid")
    val res7 = Outcomes.unexpected<Int>("Unable to send confirmation code")
    
Back to features Back to top


Try

The Try is simply a type alias for Result[T, Exception] and allows you to use Result as Try[T]. This also comes with a Tries builder to construct Successes / Failures with exceptions. This is quite similar to Scala Try. Also, while functional error-handling is prioritized in Slate Kit, its not a dogmatic / absolute approach and exceptions can be used where appropriate.

     
    import slatekit.results.* 
    import slatekit.results.builders.Tries

    // Try<Long> = Result<Long, Exception>
    val converted1:Try<Long> = Tries.attempt { "1".toLong() }

    // DeniedException will checked and converted to Status.Denied
    val converted2:Try<Long> = Tries.attemptWithStatus<Long> {
        throw DeniedException("Token invalid")
    }
Back to features Back to top


Http

Status codes not only serve to logically categories successes/failures but become a natural and easy way to convert results to platform/protocol specific errors. There is a default Err to HTTP status code converter available in Codes

     
    // Simulate a denied exception ( Security related )
    val denied:Outcome<Long> = Outcomes.denied("Access token has expired")
    
    // Convert it to HTTP
    // This returns back the HTTP code + original Status
    val code:Pair<Int, Status> = Codes.toHttp(denied.status)
    println(code.first) // 401
     
Back to features Back to top



Back to top