From 9bcaa974ca03b9f518a1e84717f79272f4d090c2 Mon Sep 17 00:00:00 2001 From: Tristan Hamilton Date: Fri, 8 May 2020 15:42:14 +0100 Subject: [PATCH] Add monad comprehensions via binding block --- .../com/github/michaelbull/result/Binding.kt | 58 +++++++++++++++ .../github/michaelbull/result/BindingTest.kt | 74 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/main/kotlin/com/github/michaelbull/result/Binding.kt create mode 100644 src/test/kotlin/com/github/michaelbull/result/BindingTest.kt diff --git a/src/main/kotlin/com/github/michaelbull/result/Binding.kt b/src/main/kotlin/com/github/michaelbull/result/Binding.kt new file mode 100644 index 0000000..5a8a3f4 --- /dev/null +++ b/src/main/kotlin/com/github/michaelbull/result/Binding.kt @@ -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 { ... } + * fun provideY(): Result { ... } + * + * val result: Result = binding { + * val x = provideX().bind() + * val y = provideY().bind() + * x + y + * } + * ``` + * + * @sample com.github.michaelbull.result.bind.ResultBindingTest + */ +inline fun binding(crossinline block: ResultBinding.() -> V): Result { + val receiver = ResultBindingImpl() + + return try { + with(receiver) { Ok(block()) } + } catch (ex: BindException) { + receiver.error + } +} + +internal object BindException : Exception() { + override fun fillInStackTrace(): Throwable { + return this + } +} + +interface ResultBinding { + fun Result.bind(): V +} + +@PublishedApi +internal class ResultBindingImpl : ResultBinding { + + lateinit var error: Err + + override fun Result.bind(): V { + return when (this) { + is Ok -> value + is Err -> { + this@ResultBindingImpl.error = this + throw BindException + } + } + } +} diff --git a/src/test/kotlin/com/github/michaelbull/result/BindingTest.kt b/src/test/kotlin/com/github/michaelbull/result/BindingTest.kt new file mode 100644 index 0000000..939693a --- /dev/null +++ b/src/test/kotlin/com/github/michaelbull/result/BindingTest.kt @@ -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 = Ok(1) + fun provideY(): Result = Ok(2) + + val result = binding { + val x = provideX().bind() + val y = provideY().bind() + x + y + } + + assertTrue(result is Ok) + assertEquals(3, result.value) + } + + @Test + fun returnsOkIfAllBindsOfDifferentTypeAreSuccessful() { + fun provideX(): Result = Ok("1") + fun provideY(x: Int): Result = Ok(x + 2) + + val result = binding { + 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 = Ok(1) + fun provideY(): Result = Err(BindingError) + fun provideZ(): Result = Ok(2) + + val result = binding { + 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 = Ok(1) + fun provideY(): Result = Err(BindingError) + fun provideZ(): Result = Ok(2) + + val result = binding { + val x = provideX().bind() + val y = provideY().bind() + val z = provideZ().bind() + x + y.toInt() + z + } + + assertTrue(result is Err) + assertEquals(BindingError, result.error) + } +}