App

Overview

Result<T,E> is a core component of Kiit and is used for accurately modeling successes and failures of any operation, 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), however Kiit Result differs significantly by allowing for a custom error type, sub-categories of successes and failures in the form of Statuses, and providing sensible defaults and ways to build errors. With Result, you can safely access the value of an operation based on its success or failure, accurately represent failures from various sources, organize errors into logical groups, and easily convert these errors into compatible errors for HTTP. The Result component removes much of the boiler plate code that you would normally create yourself to handle all these scenarios. This diagram shows the high-level design/structure:



Diagram

A high-level diagram of the concepts in this component



Goals

The design goals of Kiit differ from other implementations by allowing for a custom error type, sub-categories of successes and failures in the form of Statuses, and providing sensible defaults and ways to build errors.



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


Compare

The main differences between other implementations is that Kiit Result<T,E> offers features #3, #4, and #5 above in Goals and so the differences the the following:

1. Custom Error: The Error type E can be anything, unlike Kotlin and Swift
2. Aliases: With custom error types, you can have aliases such as typealias Try<T> = Result<T,Exception>
3. Statuses: Contains an optional Status on both Success and Failure branches to further sub-categorize successes and failures.
4. Builders: Several convenience builders to easily construct different categories of errors, and from different sources ( String, Exception, Err).

# Language Name Differences
1 Kotlin Result<T> Result Error type is Exception
2 Swift Result<T> Swift Error type is Error
3 Rust Result<T,E> Kiit differs with addition of Status, and supplied Aliases, Builders ( see docs here )
4 Scala Either<L,R> General purpose use to represent A or B, branches are Left / Right, although typically used for Success/Failure. Kiit differs in semantics by using Success instead of Right, and Failure instead of Left ( similar with Scala Try, but with a customizable error type )
5 Go GRPC_Codes Status codes in Kiit 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'
    }

Back to top



Sources

Jar slatekit.result.jar
Package slatekit.results
Sources slatekit-result
Example Example_Results.kt
Version
License
Requires See build.gradle for more info.

Back to top



Example

Short example taken from Example_Result.kt, showing the usage of Result by creating, checking, and pattern matching the values.

       
    import slatekit.results.*

    // Create success explicitly
    val start: Outcome<Int> = Success(10)

    // Properties
    println("success:     " + start.success)         // true
    println("status.code: " + start.status.code) // Codes.SUCCESS.code
    println("status.code: " + 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("contains:  " + addResult.contains(11))
    println("exists  :  " + addResult.exists { it == 11 })

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

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

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

    // Pattern match on status: Passed / Failed statuses
    when(start) {
        is Success -> when(start.status) {
            is Passed.Succeeded  -> println("Succeeded: " + start.msg)
            is Passed.Pending    -> println("Pending  : " + start.msg)
        }
        is Failure -> when(start.status) {
            is Failed.Denied     -> println("Denied    : " + start.msg)
            is Failed.Invalid    -> println("Invalid   : " + start.msg)
            is Failed.Ignored    -> println("Ignored   : " + start.msg)
            is Failed.Errored    -> println("Errored   : " + start.msg)
            is Failed.Unexpected -> println("Unexpected: " + start.msg)
        }
    }
        

Back to top



Concepts


Name Description More
1. Terms Terms and concepts in Result more
2. Result The core component for modeling successes and failures more
3. Statuses Status codes and logical grouping of states more
4. Builders Several easy ways to build success or different category of errors more
5. Aliases Type aliases to default the error type for simplicity more
6. Errors Define your error types or use pre-built ones more
7. Conversion Convert Successes / Failures to compatible protocol values more

Back to top



Terms

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 states to sub-group both successes and failures. E.g. Succeeded, Denied, Invalid, etc
Codes Optional Default implementations of general purpose statuses as codes
Aliases Optional Type aliases to simplify the Result from 2 type parameters to 1. E.g. Outcome<T> = Result<T,Err>, Try<T> = Result<T,Exception>
Builders Optional Convenient methods to build errors such as Outcomes.denied

Back to top



Result

Result<T,E> is the main component as a sealed class with only 2 implementations of Success<T> and Failure<E>. Success<T> stores the successful value of an operation, and Failure<E> stores the error of an operation.
Result has 2 type parameters T and E.
T is used to represent the type of value ( Int, User, etc ) that the Success branch will store on the successful result of an operation.
E is used to represent the type of error ( String, Exception, etc ) that the Failure branch will store on the failed result of an operation.
The branches are also sensiblly defaulted with Status codes to make them optional.

    
    import slatekit.results.*

    // Result base class
    sealed class Result<out T, out E> {
        // implementation
    }

    // Success branch ( type T to store value of successful operation )
    data class Success<out T>(val value: T, override val status: Passed = Codes.SUCCESS) 
        : Result<T, Nothing>() {
        // implementation
    }

    // Failure branch ( type E to store error of failed operation )
    data class Failure<out E>(val error: E, override val status: Failed = Codes.ERRORED) 
        : Result<Nothing, E>() {
        // implementation
    }
     
Back to features Back to top


Statuses

One of the distinguishing features of Kiit Result is the introduction of a Status component on the Result type base class. The status is applicable for both the Success / Failure branch and is made of a Integer code and String message and is used to further sub-categorize and classify successes and failures to figure out why something passed or failed. Default Codes are provided for convenience. The Status Codes provide a few main benefits:

1. Categorization Categorize result 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 convert the status to HTTP codes E.g. Succeeded -> HTTP 200 range
    package slatekit.results

    interface class Status {
        abstract val code: Int
        abstract val msg: String
    }

    sealed class Passed : Status {
        data class Succeeded (override val code: Int, override val msg: String) : Passed()
        data class Pending   (override val code: Int, override val msg: String) : Passed()
    }

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

Back to features Back to top


Aliases

The Result type has 2 type parameters (T for value and E for error), but often its more convenient to work with known error types and also to avoid having to constantly specify the error type E. This is where Kotlins typealias come in handy. There are a few type Aliases available 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. Outcome is used heavily in Kiit as we use functional error-handling and avoid throwing of Exceptions ( generally speaking ).

    import slatekit.results.* 
    
    // Aliases simplify Result from 2 type parameters down to 1.
    typealias Try<T>       = Result<T, Exception>
    typealias Notice<T>    = Result<T, String>
    typealias Outcome<T>   = Result<T, Err>
    typealias Validated<T> = Result<T, Err.ErrorList>
    
    // You can now pass around the types more simply
    val result:Try<Int> = Success( "1".toInt() )
    println(result)
Back to features Back to top


Builders

Building up Results / Successes / Failures can become a bit tedious, especially with the combination of logical Status groups and error types ( Strings, Exception, etc ). The Builders interface is available for convenience to simplify the construction of appropriate error types. There are builders Tries, Notices, Outcomes for the corresponding Aliases Try, Notice, Outcome.

    
    // Builder interface can build Result<T, E> from strings, exception, err
    interface Builder<out E> {
        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, status: Failed.Invalid? = null): Result<T, E> = Failure(errorFromEx(ex, Codes.INVALID), status ?: Codes.INVALID)
        fun <T> invalid(err: Err, status:Failed.Invalid? = null): Result<T, E> = Failure(errorFromErr(err, Codes.INVALID), status ?: Codes.INVALID)

        // misc code

    }
    // Misc builders ( See links / code )
    interface TryBuilder : Builder<Exception> { /* .. */ }
    object Tries : TryBuilder { /* ... */ }

    interface NoticeBuilder : Builder<String> { /* .. */ }
    object Notice : NoticeBuilder { /* ... */ }

    interface OutcomeBuilder : Builder<Err> { /* .. */ }
    object Outcome : OutcomeBuilder { /* ... */ }

With the builders and aliases in place, we can uses the Tries, Notices or Outcomes to easily construct accurate results

    import slatkeit.results.builders.*

    // Build results ( imagine this is some user registration flow )
    // Try<T> = Result<T, Exception>
    val tried1 = Tries.success( User() )
    val tried2 = Tries.denied<User>("Phone exists")
    val tried3 = Tries.invalid<User>("Email required")

    // Outcome<T> = Result<T, Err>
    val outcome1 = Outcomes.success( User() )
    val outcome2 = Outcomes.denied<User>("Phone exists")
    val outcome3 = Outcomes.invalid<User>("Email required" )

    // Notice<T> = Result<T, String>
    val notice1 = Notices.success( User() )
    val notice2 = Notices.denied<User>("Phone exists")
    val notice3 = Notices.invalid<User>("Email required" )
Back to features Back to top


Errors

The error type E on Result and its Failure branch can be anything, unlike other implementations which default E to Exception. While you can use String, or Exception as the error type, Kiit offers Err as a custom error type. This provides convenient ways to build errors from a string, exception, based on an invalid field or from a list of errors. This is much more comprehensive than using a simple String as an error type and more aligned with functional programming by avoiding throwing of Exceptions where applicable.

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

    // Build Err from various sources using convenience methods
    // From simple string
    val err1 = Err.of("Invalid email")

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

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

    // From status code
    val err4 = Err.code(Codes.INVALID)

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

    // From list of Err types
    val err6 = Err.ErrorList(listOf(
            Err.on("email", "abc123 is not a valid email", "Invalid email"),
            Err.on("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


Conversions

Results can be gracefully converted to other representations since there are 3 levels of detail:

Detail # Values Type(s) Conversion
Top Level 2 : pass / fail Success / Failure True / False
Mid Level 7 : logical categories of Status Status sub-classes Succeeded, Pending, Denied, Ignored, Invalid, Errored, Unexpected etc
Low Level N: Based on # of statuses you use Status.code 200001, 400001, 500001, etc
    
    // Suppose this an API endpoint to register a user

    // Simulate registration through a service layer
    val result:Outcome<User> = service.registerUser(user)

    // Convert the result back to HTTP somehow
    // NOTE: This is a simple conversion, see docs below
    when (result.status) {
        is Status.Succeeded  -> Http.Code200
        is Status.Invalid    -> Http.Code400
        is Status.Denied     -> Http.Code401
        is Status.Errored    -> Http.Code400
        else                 -> Http.Code500
    } 

Back to features Back to top


Guide

Name Description More
1. Create Create successes, failures in various ways more
2. Access Get values safely using available operations more
3. Check Check for values or use pattern matching more
4. Outcome A type alias for Result[T, Err] where Err is an error interface more
5. Try A type alias for Result[T, Exception] to work with Exceptions more
6. Validated A type alias for Result[T, Err.ErrorList] to collect errors more
7. HTTP Support Convert Successes / Failures to compatible HTTP codes more

Back to top



Create

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


Access

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 sub-categories of Successes and Failures.

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

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


Outcome

The Outcome<T> class is simply a type alias for Result<T, Err> and allows you to use Result as Outcome<T>. This Outcomes has builder methods 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<T> class is simply a type alias for Result<T, Exception> and allows you to use Result as Try<T>. This has Tries builder methods to construct Successes / Failures with exceptions. This is quite similar to Scala Try. Also, while functional error-handling is prioritized in Kiit, 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.of { "1".toLong() }

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


Validated

The Validated<T> class is simply a type alias for Result<T, Err.ErrorList> and allows you to use collect a list of Err.

     
    import slatekit.results.* 
    import slatekit.results.builders.Tries
    import slatekit.common.validations.Validations

    // Model to validate
    val user = User(0, "batman_gotham", "batman", "", true, 34)
    
    // Validated<User> = Result<User, Err.ErrorList>
    val validated = Validations.collect<User,String, Err>(user) {
        listOf(
                isNotEmpty(user.firstName),
                isNotEmpty(user.lastName),
                isEmail(user.email)
        )
    }

    // Print first error
    when(validated) {
        is Success -> println("User model is valid") 
        is Failure -> println("User model failed with : " + validated.error.errors.first().msg)
    }
Back to features Back to top


Http

Status not only serve to logically sub-categorize successes/failures but they become a natural and easy way to convert results to platform/protocol specific statuses/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