Add monad comprehensions via binding block

This commit is contained in:
Tristan Hamilton 2020-05-08 15:42:14 +01:00 committed by Michael Bull
parent 1000c588c0
commit 9bcaa974ca
2 changed files with 132 additions and 0 deletions

View File

@ -0,0 +1,58 @@
package com.github.michaelbull.result
/**
* Calls the specified function [block] with [ResultBinding] as its receiver and returns its [Result].
*
* When inside a [binding] block, the [bind][ResultBinding.bind] function is accessible on any [Result]. Calling the
* [bind][ResultBinding.bind] function will attempt to unwrap the [Result] and locally return its [value][Ok.value]. If
* the [Result] is an [Err], the binding block will terminate early and return the first [error][Err.error].
*
* Example:
* ```
* fun provideX(): Result<Int, ExampleErr> { ... }
* fun provideY(): Result<Int, ExampleErr> { ... }
*
* val result: Result<Int, ExampleErr> = binding {
* val x = provideX().bind()
* val y = provideY().bind()
* x + y
* }
* ```
*
* @sample com.github.michaelbull.result.bind.ResultBindingTest
*/
inline fun <V, E> binding(crossinline block: ResultBinding<E>.() -> V): Result<V, E> {
val receiver = ResultBindingImpl<E>()
return try {
with(receiver) { Ok(block()) }
} catch (ex: BindException) {
receiver.error
}
}
internal object BindException : Exception() {
override fun fillInStackTrace(): Throwable {
return this
}
}
interface ResultBinding<E> {
fun <V> Result<V, E>.bind(): V
}
@PublishedApi
internal class ResultBindingImpl<E> : ResultBinding<E> {
lateinit var error: Err<E>
override fun <V> Result<V, E>.bind(): V {
return when (this) {
is Ok -> value
is Err -> {
this@ResultBindingImpl.error = this
throw BindException
}
}
}
}

View File

@ -0,0 +1,74 @@
package com.github.michaelbull.result
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class BindingTest {
object BindingError
@Test
fun returnsOkIfAllBindsSuccessful() {
fun provideX(): Result<Int, BindingError> = Ok(1)
fun provideY(): Result<Int, BindingError> = Ok(2)
val result = binding<Int, BindingError> {
val x = provideX().bind()
val y = provideY().bind()
x + y
}
assertTrue(result is Ok)
assertEquals(3, result.value)
}
@Test
fun returnsOkIfAllBindsOfDifferentTypeAreSuccessful() {
fun provideX(): Result<String, BindingError> = Ok("1")
fun provideY(x: Int): Result<Int, BindingError> = Ok(x + 2)
val result = binding<Int, BindingError> {
val x = provideX().bind()
val y = provideY(x.toInt()).bind()
y
}
assertTrue(result is Ok)
assertEquals(3, result.value)
}
@Test
fun returnsFirstErrIfBindingFailed() {
fun provideX(): Result<Int, BindingError> = Ok(1)
fun provideY(): Result<Int, BindingError> = Err(BindingError)
fun provideZ(): Result<Int, BindingError> = Ok(2)
val result = binding<Int, BindingError> {
val x = provideX().bind()
val y = provideY().bind()
val z = provideZ().bind()
x + y + z
}
assertTrue(result is Err)
assertEquals(BindingError, result.error)
}
@Test
fun returnsFirstErrIfBindingsOfDifferentTypesFailed() {
fun provideX(): Result<Int, BindingError> = Ok(1)
fun provideY(): Result<String, BindingError> = Err(BindingError)
fun provideZ(): Result<Int, BindingError> = Ok(2)
val result = binding<Int, BindingError> {
val x = provideX().bind()
val y = provideY().bind()
val z = provideZ().bind()
x + y.toInt() + z
}
assertTrue(result is Err)
assertEquals(BindingError, result.error)
}
}