Skip to content

Commit 522886e

Browse files
authored
Added a resolve function to Either. (#237)
* Added a resolve function to Either. * Processed some review comments. * Added docs for catch and resolve functions. * Improved docs for the resolve function. * Inlined catch and resolve functions. * Changed the implementation of the resolve function to not use default values for function parameters so that all function parameters can be inlined. * Changed the handleItSafely function into catchAndFlatten. * Made small improvement to tests.
1 parent c536bff commit 522886e

File tree

3 files changed

+320
-1
lines changed

3 files changed

+320
-1
lines changed

arrow-libs/core/arrow-core-data/src/main/kotlin/arrow/core/Either.kt

+142-1
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,121 @@ import arrow.typeclasses.Show
314314
* }
315315
* ```
316316
*
317+
* ## Either.catch exceptions
318+
*
319+
* Sometimes you do need to interact with code that can potentially throw exceptions. In such cases, you should mitigate the possibility that an exception can be thrown. You can do so by using the `catch` function.
320+
*
321+
* Example:
322+
*
323+
* ```kotlin:ank:playground
324+
* import arrow.core.Either
325+
*
326+
* //sampleStart
327+
* fun potentialThrowingCode(): String = throw RuntimeException("Blow up!")
328+
*
329+
* suspend fun makeSureYourLogicDoesNotHaveSideEffects(): Either<Error, String> =
330+
* Either.catch { potentialThrowingCode() }.mapLeft { Error.SpecificError }
331+
* //sampleEnd
332+
* suspend fun main() {
333+
* println("makeSureYourLogicDoesNotHaveSideEffects().isLeft() = ${makeSureYourLogicDoesNotHaveSideEffects().isLeft()}")
334+
* }
335+
*
336+
* sealed class Error {
337+
* object SpecificError : Error()
338+
* }
339+
* ```
340+
*
341+
* ## Resolve Either into one type of value
342+
* In some cases you can not use Either as a value. For instance, when you need to respond to an HTTP request. To resolve Either into one type of value, you can use the resolve function.
343+
* In the case of an HTTP endpoint you most often need to return some (framework specific) response object which holds the result of the request. The result can be expected and positive, this is the success flow.
344+
* Or the result can be expected but negative, this is the error flow. Or the result can be unexpected and negative, in this case an unhandled exception was thrown.
345+
* In all three cases, you want to use the same kind of response object. But probably you want to respond slightly different in each case. This can be achieved by providing specific functions for the success, error and throwable cases.
346+
*
347+
* Example:
348+
*
349+
* ```kotlin:ank:playground
350+
* import arrow.core.Either
351+
* import arrow.core.flatMap
352+
* import arrow.core.left
353+
* import arrow.core.right
354+
*
355+
* //sampleStart
356+
* suspend fun httpEndpoint(request: String = "Hello?") =
357+
* Either.resolve(
358+
* f = {
359+
* if (request == "Hello?") "HELLO WORLD!".right()
360+
* else Error.SpecificError.left()
361+
* },
362+
* success = { a -> handleSuccess({ a: Any -> log(Level.INFO, "This is a: $a") }, a) },
363+
* error = { e -> handleError({ e: Any -> log(Level.WARN, "This is e: $e") }, e) },
364+
* throwable = { throwable -> handleThrowable({ throwable: Throwable -> log(Level.ERROR, "Log the throwable: $throwable.") }, throwable) },
365+
* unrecoverableState = { _ -> Unit.right() }
366+
* )
367+
* //sampleEnd
368+
* suspend fun main() {
369+
* println("httpEndpoint().status = ${httpEndpoint().status}")
370+
* }
371+
*
372+
* @Suppress("UNUSED_PARAMETER")
373+
* suspend fun <A> handleSuccess(log: suspend (a: A) -> Either<Throwable, Unit>, a: A): Either<Throwable, Response> =
374+
* Either.catch {
375+
* Response.Builder(HttpStatus.OK)
376+
* .header(CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON)
377+
* .body(a)
378+
* .build()
379+
* }
380+
*
381+
* @Suppress("UNUSED_PARAMETER")
382+
* suspend fun <E> handleError(log: suspend (e: E) -> Either<Throwable, Unit>, e: E): Either<Throwable, Response> =
383+
* createErrorResponse(HttpStatus.NOT_FOUND, ErrorResponse("$ERROR_MESSAGE_PREFIX $e"))
384+
*
385+
* suspend fun handleThrowable(log: suspend (throwable: Throwable) -> Either<Throwable, Unit>, throwable: Throwable): Either<Throwable, Response> =
386+
* log(throwable)
387+
* .flatMap { createErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, ErrorResponse("$THROWABLE_MESSAGE_PREFIX $throwable")) }
388+
*
389+
* suspend fun createErrorResponse(httpStatus: HttpStatus, errorResponse: ErrorResponse): Either<Throwable, Response> =
390+
* Either.catch {
391+
* Response.Builder(httpStatus)
392+
* .header(CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON)
393+
* .body(errorResponse)
394+
* .build()
395+
* }
396+
*
397+
* suspend fun log(level: Level, message: String): Either<Throwable, Unit> =
398+
* Unit.right() // Should implement logging.
399+
*
400+
* enum class HttpStatus(val value: Int) { OK(200), NOT_FOUND(404), INTERNAL_SERVER_ERROR(500) }
401+
*
402+
* class Response private constructor(
403+
* val status: HttpStatus,
404+
* val headers: Map<String, String>,
405+
* val body: Any?
406+
* ) {
407+
*
408+
* data class Builder(
409+
* val status: HttpStatus,
410+
* var headers: Map<String, String> = emptyMap(),
411+
* var body: Any? = null
412+
* ) {
413+
* fun header(key: String, value: String) = apply { this.headers = this.headers + mapOf<String, String>(key to value) }
414+
* fun body(body: Any?) = apply { this.body = body }
415+
* fun build() = Response(status, headers, body)
416+
* }
417+
* }
418+
*
419+
* val CONTENT_TYPE = "Content-Type"
420+
* val CONTENT_TYPE_APPLICATION_JSON = "application/json"
421+
* val ERROR_MESSAGE_PREFIX = "An error has occurred. The error is:"
422+
* val THROWABLE_MESSAGE_PREFIX = "An exception was thrown. The exception is:"
423+
* sealed class Error {
424+
* object SpecificError : Error()
425+
* }
426+
* data class ErrorResponse(val errorMessage: String)
427+
* enum class Level { INFO, WARN, ERROR }
428+
* ```
429+
*
430+
* There are far more use cases for the resolve function, the HTTP endpoint example is just one of them.
431+
*
317432
* ## Syntax
318433
*
319434
* Either can also map over the `left` value with `mapLeft`, which is similar to map, but applies on left instances.
@@ -912,20 +1027,46 @@ sealed class Either<out A, out B> : EitherOf<A, B> {
9121027
inline fun <L, R> conditionally(test: Boolean, ifFalse: () -> L, ifTrue: () -> R): Either<L, R> =
9131028
if (test) right(ifTrue()) else left(ifFalse())
9141029

915-
suspend fun <R> catch(f: suspend () -> R): Either<Throwable, R> =
1030+
suspend inline fun <R> catch(f: suspend () -> R): Either<Throwable, R> =
9161031
try {
9171032
f().right()
9181033
} catch (t: Throwable) {
9191034
t.nonFatalOrThrow().left()
9201035
}
9211036

1037+
suspend inline fun <R> catchAndFlatten(f: suspend () -> Either<Throwable, R>): Either<Throwable, R> =
1038+
catch(f).fold({ it.left() }, { it })
1039+
9221040
@Deprecated("Use catch with mapLeft instead", ReplaceWith("catch(f).mapLeft(fe)"))
9231041
suspend fun <L, R> catch(fe: (Throwable) -> L, f: suspend () -> R): Either<L, R> =
9241042
try {
9251043
f().right()
9261044
} catch (t: Throwable) {
9271045
fe(t.nonFatalOrThrow()).left()
9281046
}
1047+
1048+
/**
1049+
* The resolve function can resolve any suspended function that yields an Either into one type of value.
1050+
*
1051+
* @param f the function that needs to be resolved.
1052+
* @param success the function to apply if [f] yields a success of type [A].
1053+
* @param error the function to apply if [f] yields an error of type [E].
1054+
* @param throwable the function to apply if [f] throws a [Throwable].
1055+
* Throwing any [Throwable] in the [throwable] function will render the [resolve] function nondeterministic.
1056+
* @param unrecoverableState the function to apply if [resolve] is in an unrecoverable state.
1057+
* @return the result of applying the [resolve] function.
1058+
*/
1059+
suspend inline fun <E, A, B> resolve(
1060+
f: suspend () -> Either<E, A>,
1061+
success: suspend (a: A) -> Either<Throwable, B>,
1062+
error: suspend (e: E) -> Either<Throwable, B>,
1063+
throwable: suspend (throwable: Throwable) -> Either<Throwable, B>,
1064+
unrecoverableState: suspend (throwable: Throwable) -> Either<Throwable, Unit>
1065+
): B =
1066+
catch(f)
1067+
.fold({ t: Throwable -> throwable(t) }, { it.fold({ e: E -> catchAndFlatten { error(e) } }, { a: A -> catchAndFlatten { success(a) } }) })
1068+
.fold({ t: Throwable -> throwable(t) }, { b: B -> b.right() })
1069+
.fold({ t: Throwable -> unrecoverableState(t); throw t }, { b: B -> b })
9291070
}
9301071
}
9311072

arrow-libs/core/arrow-core-data/src/test/kotlin/arrow/core/EitherTest.kt

+138
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,17 @@ import arrow.core.extensions.monoid
2626
import arrow.core.extensions.order
2727
import arrow.core.extensions.show
2828
import arrow.core.test.UnitSpec
29+
import arrow.core.test.generators.any
2930
import arrow.core.test.generators.either
3031
import arrow.core.test.generators.genK
3132
import arrow.core.test.generators.genK2
3233
import arrow.core.test.generators.id
3334
import arrow.core.test.generators.intSmall
35+
import arrow.core.test.generators.suspendFunThatReturnsAnyLeft
36+
import arrow.core.test.generators.suspendFunThatReturnsAnyRight
37+
import arrow.core.test.generators.suspendFunThatReturnsEitherAnyOrAnyOrThrows
38+
import arrow.core.test.generators.suspendFunThatThrows
39+
import arrow.core.test.generators.suspendFunThatThrowsFatalThrowable
3440
import arrow.core.test.generators.throwable
3541
import arrow.core.test.laws.BicrosswalkLaws
3642
import arrow.core.test.laws.BifunctorLaws
@@ -48,6 +54,8 @@ import arrow.typeclasses.Eq
4854
import io.kotlintest.properties.Gen
4955
import io.kotlintest.properties.forAll
5056
import io.kotlintest.shouldBe
57+
import io.kotlintest.shouldThrow
58+
import kotlinx.coroutines.runBlocking
5159

5260
class EitherTest : UnitSpec() {
5361

@@ -238,5 +246,135 @@ class EitherTest : UnitSpec() {
238246
suspend fun loadFromNetwork(): Int = throw exception
239247
Either.catch { loadFromNetwork() } shouldBe Left(exception)
240248
}
249+
250+
"catchAndFlatten should return Right(result) when f does not throw" {
251+
suspend fun loadFromNetwork(): Either<Throwable, Int> = Right(1)
252+
Either.catchAndFlatten { loadFromNetwork() } shouldBe Right(1)
253+
}
254+
255+
"catchAndFlatten should return Left(result) when f throws" {
256+
val exception = Exception("Boom!")
257+
suspend fun loadFromNetwork(): Either<Throwable, Int> = throw exception
258+
Either.catchAndFlatten { loadFromNetwork() } shouldBe Left(exception)
259+
}
260+
261+
"resolve should yield a result when deterministic functions are used as handlers" {
262+
forAll(
263+
Gen.suspendFunThatReturnsEitherAnyOrAnyOrThrows(),
264+
Gen.any()
265+
) { f: suspend () -> Either<Any, Any>,
266+
returnObject: Any ->
267+
268+
runBlocking {
269+
val result =
270+
Either.resolve(
271+
f = f,
272+
success = { a -> handleWithPureFunction(a, returnObject) },
273+
error = { e -> handleWithPureFunction(e, returnObject) },
274+
throwable = { t -> handleWithPureFunction(t, returnObject) },
275+
unrecoverableState = ::handleWithPureFunction
276+
)
277+
result == returnObject
278+
}
279+
}
280+
}
281+
282+
"resolve should throw a Throwable when a fatal Throwable is thrown" {
283+
forAll(
284+
Gen.suspendFunThatThrowsFatalThrowable(),
285+
Gen.any()
286+
) { f: suspend () -> Either<Any, Any>,
287+
returnObject: Any ->
288+
289+
runBlocking {
290+
shouldThrow<Throwable> {
291+
Either.resolve(
292+
f = f,
293+
success = { a -> handleWithPureFunction(a, returnObject) },
294+
error = { e -> handleWithPureFunction(e, returnObject) },
295+
throwable = { t -> handleWithPureFunction(t, returnObject) },
296+
unrecoverableState = ::handleWithPureFunction
297+
)
298+
}
299+
}
300+
true
301+
}
302+
}
303+
304+
"resolve should yield a result when an exception is thrown in the success supplied function" {
305+
forAll(
306+
Gen.suspendFunThatReturnsAnyRight(),
307+
Gen.any()
308+
) { f: suspend () -> Either<Any, Any>,
309+
returnObject: Any ->
310+
311+
runBlocking {
312+
val result =
313+
Either.resolve(
314+
f = f,
315+
success = ::throwException,
316+
error = { e -> handleWithPureFunction(e, returnObject) },
317+
throwable = { t -> handleWithPureFunction(t, returnObject) },
318+
unrecoverableState = ::handleWithPureFunction
319+
)
320+
result == returnObject
321+
}
322+
}
323+
}
324+
325+
"resolve should yield a result when an exception is thrown in the error supplied function" {
326+
forAll(
327+
Gen.suspendFunThatReturnsAnyLeft(),
328+
Gen.any()
329+
) { f: suspend () -> Either<Any, Any>,
330+
returnObject: Any ->
331+
332+
runBlocking {
333+
val result =
334+
Either.resolve(
335+
f = f,
336+
success = { a -> handleWithPureFunction(a, returnObject) },
337+
error = ::throwException,
338+
throwable = { t -> handleWithPureFunction(t, returnObject) },
339+
unrecoverableState = ::handleWithPureFunction
340+
)
341+
result == returnObject
342+
}
343+
}
344+
}
345+
346+
"resolve should throw a Throwable when any exception is thrown in the throwable supplied function" {
347+
forAll(
348+
Gen.suspendFunThatThrows()
349+
) { f: suspend () -> Either<Any, Any> ->
350+
351+
runBlocking {
352+
shouldThrow<Throwable> {
353+
Either.resolve(
354+
f = f,
355+
success = ::throwException,
356+
error = ::throwException,
357+
throwable = ::throwException,
358+
unrecoverableState = ::handleWithPureFunction
359+
)
360+
}
361+
}
362+
true
363+
}
364+
}
241365
}
242366
}
367+
368+
@Suppress("RedundantSuspendModifier", "UNUSED_PARAMETER")
369+
suspend fun handleWithPureFunction(a: Any, b: Any): Either<Throwable, Any> =
370+
b.right()
371+
372+
@Suppress("RedundantSuspendModifier", "UNUSED_PARAMETER")
373+
suspend fun handleWithPureFunction(throwable: Throwable): Either<Throwable, Unit> =
374+
Unit.right()
375+
376+
@Suppress("RedundantSuspendModifier", "UNUSED_PARAMETER")
377+
private suspend fun <A> throwException(
378+
a: A
379+
): Either<Throwable, Any> =
380+
throw RuntimeException("An Exception is thrown while handling the result of the supplied function.")

arrow-libs/core/arrow-core-test/src/main/kotlin/arrow/core/test/generators/Generators.kt

+40
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import arrow.core.extensions.listk.semialign.semialign
3131
import arrow.core.extensions.sequencek.semialign.semialign
3232
import arrow.core.fix
3333
import arrow.core.k
34+
import arrow.core.left
35+
import arrow.core.right
3436
import arrow.core.toOption
3537
import arrow.typeclasses.Applicative
3638
import arrow.typeclasses.ApplicativeError
@@ -201,3 +203,41 @@ private fun <A, B, R> Gen<A>.alignWith(genB: Gen<B>, transform: (Ior<A, B>) -> R
201203
alignWith(this@alignWith.random().k(), genB.random().k(), transform)
202204
}.fix()
203205
}
206+
207+
fun Gen.Companion.suspendFunThatReturnsEitherAnyOrAnyOrThrows(): Gen<suspend () -> Either<Any, Any>> =
208+
oneOf(
209+
suspendFunThatReturnsAnyRight(),
210+
suspendFunThatReturnsAnyLeft(),
211+
suspendFunThatThrows()
212+
)
213+
214+
fun Gen.Companion.suspendFunThatReturnsAnyRight(): Gen<suspend () -> Either<Any, Any>> =
215+
any().map { suspend { it.right() } }
216+
217+
fun Gen.Companion.suspendFunThatReturnsAnyLeft(): Gen<suspend () -> Either<Any, Any>> =
218+
any().map { suspend { it.left() } }
219+
220+
fun Gen.Companion.suspendFunThatThrows(): Gen<suspend () -> Either<Any, Any>> =
221+
throwable().map { suspend { throw it } } as Gen<suspend () -> Either<Any, Any>>
222+
223+
fun Gen.Companion.suspendFunThatThrowsFatalThrowable(): Gen<suspend () -> Either<Any, Any>> =
224+
fatalThrowable().map { suspend { throw it } } as Gen<suspend () -> Either<Any, Any>>
225+
226+
fun Gen.Companion.any(): Gen<Any> =
227+
oneOf(
228+
string() as Gen<Any>,
229+
int() as Gen<Any>,
230+
long() as Gen<Any>,
231+
float() as Gen<Any>,
232+
double() as Gen<Any>,
233+
bool() as Gen<Any>,
234+
uuid() as Gen<Any>,
235+
file() as Gen<Any>,
236+
localDate() as Gen<Any>,
237+
localTime() as Gen<Any>,
238+
localDateTime() as Gen<Any>,
239+
period() as Gen<Any>,
240+
throwable() as Gen<Any>,
241+
fatalThrowable() as Gen<Any>,
242+
unit() as Gen<Any>
243+
)

0 commit comments

Comments
 (0)