State

Small library for value-based outcome representation.

Using state, it is possible to model a successful operation, a failed operation, as well as the intermediate progress states of an ongoing operation (using progress).

Representing a computation's outcome

In Kotlin, exceptions are not an idiomatic way to represent domain failures. They should only be used for programming errors (for example, broken invariants, using IllegalArgumentException), issues that may arise anywhere in a program and thus shouldn't be handled everywhere they appear (for example, OutOfMemoryError) or to encapsulate errors outside our control (for example, NetworkError). Instead, we should represent the outcome of a domain operation using a sealed class hierarchy.

Multiple libraries have been created to facilitate using these sealed class hierarchies, most notably Arrow Typed Errors. These libraries provide enhanced semantics and syntax sugar for this style of error management, but they lack a representation for intermediate values (for example, the current progress of an information).

Pedestal State is built on top of Arrow, and adds:

Example

Let's compare a simple account creation screen; we want to verify that the data is correct and display the results to a user. First, let's see how it could be implemented with the traditional Kotlin approach of using a sealed hierarchy:

// ①. Declare all possible outcomes
sealed class AccountCreationResult {
data class Success(val user: User) : AccountCreationResult()
data object PasswordTooShort : AccountCreationResult()
data object PasswordsDoNotMatch : AccountCreationResult()
// …
}

// ②. Implement the method
suspend fun createAccount(username: String, password: String, passwordCopy: String): AccountCreationResult {
// …data checking…
if (password.length < 4)
return AccountCreationResult.PasswordTooShort
// note that the conditions are reversed from what we want!

if (password != passwordCopy)
return AccountCreationResult.PasswordsDoNotMatch

// …

return AccountCreationResult.Success(repository.createAccount(username, password))
}

// ③. Call site
when (val result = createAccount(username, password, passwordCopy)) {
is Success -> {
println("Created the user $it")
}
else -> {
println("Could not create user: $it")
}
}

Now, let's rewrite this using State:

// ①. Declare the failure reasons (no need to declare the successful case)
sealed class AccountCreationFailure {
data object PasswordTooShort : AccountCreationFailure()
data object PasswordsDoNotMatch : AccountCreationFailure()
}

// ②. Implement the method
suspend fun createAccount(username: String, password: String, passwordCopy: String) = out<AccountCreationFailure, User> {
// …data checking…
ensure(password.length >= 4) { AccountCreationFailure.PasswordTooShort }
ensure(password != passwordCopy) { AccountCreationFailure.PasswordsDoNotMatch }
// note that the conditions are in a better order

// …

repository.createAccount(username, password) // no boilerplate
}

// ③. Call site
val result = createAccount(username, password, passwordCopy)

result.onSuccess {
println("Created the user $it")
}

result.onFailure {
println("Could not create user: $it")
}

As we can see, validation is much easier to read, and the call site offers more flexibility.

Packages

Link copied to clipboard
common
Link copied to clipboard
common

Helpers to convert outcomes to other Arrow data types, support for the Raise DSL.

Link copied to clipboard
common

Utilities for the Outcome type, allowing to embed typed error management directly into the API without using exceptions.

Link copied to clipboard
common

The ProgressiveOutcome, which combines Outcome with Progress.