Skip to content

Instantly share code, notes, and snippets.

@edreyer
Last active April 7, 2025 19:35
Show Gist options
  • Save edreyer/b7fc887bb2ac0b8f1b7234a4a460afd9 to your computer and use it in GitHub Desktop.
Save edreyer/b7fc887bb2ac0b8f1b7234a4a460afd9 to your computer and use it in GitHub Desktop.
Preview of context parameters
package com.example.bridge
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
// Functions useful for bridging from context receivers to the new context parameters
// This is temporary and will not be required (or provided by library authors) in the future
context(raise: Raise<Err>)
inline fun <Err> raise(r: Err): Nothing = raise.raise(r)
context(raise: Raise<Err>)
inline fun <Err> ensure(condition: Boolean, r: () -> Err): Unit = raise.ensure(condition, r)
context(raise: Raise<Err>)
inline fun <A, Err> ensureNotNull(value: A?, r: () -> Err): A = raise.ensureNotNull(value, r)
context(r: Raise<Err>)
inline fun <Err, A> Either<Err, A>.bind(): A = with(r) { bind() }
package com.example
sealed interface ExecutionError
data class ErrorParent(val message: String) : ExecutionError
data class ErrorChild(val message: String) : ExecutionError
package com.example.contextparams
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.either
import com.example.ErrorChild
import com.example.ErrorParent
import com.example.ExecutionError
import com.example.bridge.ensure
import com.example.justeither.BusinessLogic
// Using the new context parameters feature in Kotlin 2.1.20
// This shows how you can do functional error handling in a way that has
// semantics similar to throwing exceptions but differs in the following ways:
// * errors are always "returned". That is, they are part of the method signature by virtue
// of the compile time enforced Raise context which must be provided
// * But even though errors are "returned" from all functions, you only have to deal with them at the point
// where the Raise context is provided. This would be similar to where a try..catch block would be
// * this mechanism frees you from having deeply nested calls from having every level in the stack
// to deal with the error. You can just "raise" the error and it will bubble up to the caller
// * One advantage of this is that Errors don't have to be exceptions. They can be any type. Typically
// this would be an ADT structure with a sealed interface and data classes for each error type.
// * Another advantage is that this approach is that you write happy path code, but the typesafe error
// handling is baked in and enforced at compile time. This provides the same "convenience" of using
// exceptions but is much more typesafe and forces the engineer to consider failure modes while happy
// path code is being written. THis is in contrast to exceptions which are often an afterthought.
fun main() {
val result: Either<ExecutionError, Int> = either { BusinessLogic.doWorkParent(12) }
result
.onLeft { println(it) }
.onRight { println(it) }
val result2: Either<ExecutionError, Int> = either { BusinessLogic.doWorkParent(8) }
result2
.onLeft { println(it) }
.onRight { println(it) }
}
object BusinessLogic {
context(_: Raise<ExecutionError>)
fun doWorkParent(n: Int): Int {
val childResult: Int = doWorkChild(n)
ensure (childResult % 2 == 0) { ErrorParent("n is not divisible by 2") }
return n
}
context(_: Raise<ExecutionError>)
fun doWorkChild(n: Int): Int {
val grandChildResult = doEvenMoreWorkChild(n)
ensure (grandChildResult % 4 == 0) { ErrorChild("n is not divisible by 4") }
return grandChildResult
}
context(_: Raise<ExecutionError>)
fun doEvenMoreWorkChild(n: Int): Int {
ensure (n % 3 == 0) { ErrorChild("n is not divisible by 3") }
return n
}
}
package com.example.justeither
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.example.ExecutionError
import com.example.ErrorParent
import com.example.ErrorChild
// Showing typed error handling with plain ol' Either plus the Either DSL
fun main() {
val result: Either<ExecutionError, Int> = BusinessLogic.doWorkParent(12)
result
.onLeft { println(it) }
.onRight { println(it) }
val result2: Either<ExecutionError, Int> = BusinessLogic.doWorkParent(8)
result2
.onLeft { println(it) }
.onRight { println(it) }
}
object BusinessLogic {
fun doWorkParent(n: Int): Either<ExecutionError, Int> = either {
val childResult = doWorkChild(n).bind()
ensure(childResult % 2 == 0) { ErrorParent("n is not divisible by 2") }
childResult
}
fun doWorkChild(n: Int): Either<ExecutionError, Int> = either {
val grandChildResult = doEvenMoreWorkChild(n).bind()
ensure (grandChildResult % 4 == 0) { ErrorChild("n is not divisible by 4") }
grandChildResult
}
fun doEvenMoreWorkChild(n: Int): Either<ExecutionError, Int> = either {
ensure (n % 3 == 0) { ErrorChild("n is not divisible by 3") }
n
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment