From bde7ec63a0011a8b56bc3787442f30331c1f2c07 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Wed, 7 Sep 2022 17:22:06 +0100 Subject: [PATCH 01/10] Add Resource.allocated() to decompose Resource into it's allocate and release Decomposes a [Resource] into a Pair A, suspend (A, ExitCase) -> Unit>, containing the allocation and release functions which make up the [Resource]. This can be used to integrate Resources with code which cannot be run within the [use] function. --- .../kotlin/arrow/fx/coroutines/Resource.kt | 62 +++++++++++---- .../arrow/fx/coroutines/ResourceTest.kt | 78 +++++++++++++++---- 2 files changed, 112 insertions(+), 28 deletions(-) diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt index 8fef5fbe0b6..2fe1dc40d25 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt @@ -5,13 +5,13 @@ import arrow.core.continuations.update import arrow.core.identity import arrow.core.prependTo import arrow.fx.coroutines.continuations.ResourceScope -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext import kotlin.experimental.ExperimentalTypeInference /** @@ -318,7 +318,7 @@ public sealed class Resource { public inline fun zip( b: Resource, c: Resource, - crossinline map: (A, B, C) -> D, + crossinline map: (A, B, C) -> D ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind()) @@ -328,7 +328,7 @@ public sealed class Resource { b: Resource, c: Resource, d: Resource, - crossinline map: (A, B, C, D) -> E, + crossinline map: (A, B, C, D) -> E ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind()) @@ -339,7 +339,7 @@ public sealed class Resource { c: Resource, d: Resource, e: Resource, - crossinline map: (A, B, C, D, E) -> G, + crossinline map: (A, B, C, D, E) -> G ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind()) @@ -351,7 +351,7 @@ public sealed class Resource { d: Resource, e: Resource, f: Resource, - crossinline map: (A, B, C, D, E, F) -> G, + crossinline map: (A, B, C, D, E, F) -> G ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind(), f.bind()) @@ -364,7 +364,7 @@ public sealed class Resource { e: Resource, f: Resource, g: Resource, - crossinline map: (A, B, C, D, E, F, G) -> H, + crossinline map: (A, B, C, D, E, F, G) -> H ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind()) @@ -378,7 +378,7 @@ public sealed class Resource { f: Resource, g: Resource, h: Resource, - crossinline map: (A, B, C, D, E, F, G, H) -> I, + crossinline map: (A, B, C, D, E, F, G, H) -> I ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind(), h.bind()) @@ -393,7 +393,7 @@ public sealed class Resource { g: Resource, h: Resource, i: Resource, - crossinline map: (A, B, C, D, E, F, G, H, I) -> J, + crossinline map: (A, B, C, D, E, F, G, H, I) -> J ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind(), h.bind(), i.bind()) @@ -409,7 +409,7 @@ public sealed class Resource { h: Resource, i: Resource, j: Resource, - crossinline map: (A, B, C, D, E, F, G, H, I, J) -> K, + crossinline map: (A, B, C, D, E, F, G, H, I, J) -> K ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind(), h.bind(), i.bind(), j.bind()) @@ -464,12 +464,48 @@ public sealed class Resource { public fun parZip( ctx: CoroutineContext = Dispatchers.Default, fb: Resource, - f: suspend (A, B) -> C, + f: suspend (A, B) -> C ): Resource = arrow.fx.coroutines.continuations.resource { parZip(ctx, { this@Resource.bind() }, { fb.bind() }) { a, b -> f(a, b) } } + /** + * Decomposes a [Resource] into a Pair A, suspend (A, ExitCase) -> Unit>, containing + * the allocation and release functions which make up the [Resource]. + * + * This can be used to integrate Resources with code which cannot be run within the [use] function. + */ + public suspend fun allocated(): Pair A, suspend (@UnsafeVariance A, ExitCase) -> Unit> = + when (this) { + is Bind<*, A> -> + Dsl { + val any = source.bind() + val ff = f as (Any?) -> Resource + ff(any).bind() + }.allocated() + is Allocate -> acquire to release + is Defer -> resource().allocated() + is Dsl -> { + val effect = ResourceScopeImpl() + val allocated = try { + val allocate: suspend () -> A = suspend { dsl(effect) } + val release: suspend (A, ExitCase) -> Unit = { _, e -> + effect.finalizers.get().cancelAll(e) + Unit + } + allocate to release + } catch (e: Throwable) { + val ex = if (e is CancellationException) ExitCase.Cancelled(e) else ExitCase.Failure(e) + val ee = withContext(NonCancellable) { + effect.finalizers.get().cancelAll(ex, e) ?: e + } + throw ee + } + allocated + } + } + @Deprecated( "Bind is being deprecated. Use resource DSL instead", ReplaceWith( @@ -481,7 +517,7 @@ public sealed class Resource { public class Allocate( public val acquire: suspend () -> A, - public val release: suspend (A, ExitCase) -> Unit, + public val release: suspend (A, ExitCase) -> Unit ) : Resource() @Deprecated( @@ -521,7 +557,7 @@ public sealed class Resource { */ public operator fun invoke( acquire: suspend () -> A, - release: suspend (A, ExitCase) -> Unit, + release: suspend (A, ExitCase) -> Unit ): Resource = Allocate(acquire, release) /** @@ -775,7 +811,7 @@ private class ResourceScopeImpl : ResourceScope { private suspend fun List Unit>.cancelAll( exitCase: ExitCase, - first: Throwable? = null, + first: Throwable? = null ): Throwable? = fold(first) { acc, finalizer -> val other = kotlin.runCatching { finalizer(exitCase) }.exceptionOrNull() other?.let { diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt index f68e052ed3d..004bd250f10 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.async import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList +import kotlin.random.Random class ResourceTest : ArrowFxSpec( spec = { @@ -217,15 +218,19 @@ class ResourceTest : ArrowFxSpec( val res = if (isLeft) Resource({ latch.await() shouldBe (1..depth).sum() throw cancel - }) { _, _ -> }.parZip(resource.flatMap { - Resource({ require(latch.complete(it)) }) { _, _ -> } - }) { _, _ -> } + }) { _, _ -> }.parZip( + resource.flatMap { + Resource({ require(latch.complete(it)) }) { _, _ -> } + } + ) { _, _ -> } else resource.flatMap { Resource({ require(latch.complete(it)) }) { _, _ -> } - }.parZip(Resource({ - latch.await() shouldBe (1..depth).sum() - throw cancel - }) { _, _ -> }) { _, _ -> } + }.parZip( + Resource({ + latch.await() shouldBe (1..depth).sum() + throw cancel + }) { _, _ -> } + ) { _, _ -> } res.use { fail("It should never reach here") } }.shouldBeTypeOf() @@ -265,9 +270,11 @@ class ResourceTest : ArrowFxSpec( started.await() throw cancel }) { _, _ -> } - .parZip(Resource({ require(started.complete(Unit)); i }, { ii, ex -> - require(released.complete(ii to ex)) - })) { _, _ -> } + .parZip( + Resource({ require(started.complete(Unit)); i }, { ii, ex -> + require(released.complete(ii to ex)) + }) + ) { _, _ -> } .use { fail("It should never reach here") } }.shouldBeTypeOf() @@ -310,7 +317,8 @@ class ResourceTest : ArrowFxSpec( Resource( { require(started.complete(Unit)); i }, { ii, ex -> require(released.complete(ii to ex)) } - )) { _, _ -> } + ) + ) { _, _ -> } .use { fail("It should never reach here") } } shouldBe throwable @@ -426,10 +434,12 @@ class ResourceTest : ArrowFxSpec( modifyGate.await() r.update { i -> "$i$a" } }) { _, _ -> } - .parZip(Resource({ - r.set("$b") - require(modifyGate.complete(0)) - }) { _, _ -> }) { _a, _b -> _a to _b } + .parZip( + Resource({ + r.set("$b") + require(modifyGate.complete(0)) + }) { _, _ -> } + ) { _a, _b -> _a to _b } .use { r.get() shouldBe "$b$a" } @@ -443,6 +453,44 @@ class ResourceTest : ArrowFxSpec( r.asFlow().map { it + 1 }.toList() shouldBe listOf(n + 1) } } + + suspend fun checkAllocated(mkResource: (() -> Int, (Int, ExitCase) -> Unit) -> Resource) { + val released = CompletableDeferred() + val releaseValue = Random.nextInt() + val seed = Random.nextInt() + + val (allocate, release) = mkResource({ seed }) { _, _ -> released.complete(releaseValue) }.allocated() + + allocate() shouldBe seed + + release(1, ExitCase.Completed) + + released.getCompleted() shouldBe releaseValue + } + + "allocated - Allocate" { + checkAllocated { allocate, release -> + Resource(allocate, release) + } + } + + "allocated - Defer" { + checkAllocated { allocate, release -> + Resource.defer { Resource(allocate, release) } + } + } + + "allocated - Bind" { + checkAllocated { allocate, release -> + Resource.Bind(Resource(allocate, release)) { Resource.just(it) } + } + } + + "allocated - Dsl" { + checkAllocated { allocate, close -> + arrow.fx.coroutines.continuations.resource { allocate() } releaseCase (close) + } + } } ) From 7a3a40b67f63e5370eff6b733529c0dfbc00c8f0 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Thu, 8 Sep 2022 10:13:05 +0100 Subject: [PATCH 02/10] Update arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt Co-authored-by: Simon Vergauwen --- .../src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt index 2fe1dc40d25..8d699ac125f 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt @@ -491,8 +491,7 @@ public sealed class Resource { val allocated = try { val allocate: suspend () -> A = suspend { dsl(effect) } val release: suspend (A, ExitCase) -> Unit = { _, e -> - effect.finalizers.get().cancelAll(e) - Unit + effect.finalizers.get().cancelAll(e)?.let { throw it } } allocate to release } catch (e: Throwable) { From 6d4eaa47f469f30cf5ffce445febac839f3c0873 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Thu, 8 Sep 2022 10:14:02 +0100 Subject: [PATCH 03/10] Add DelicateCoroutinesApi to allocated --- .../src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt index 2fe1dc40d25..98c6ff20830 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt @@ -6,6 +6,7 @@ import arrow.core.identity import arrow.core.prependTo import arrow.fx.coroutines.continuations.ResourceScope import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.Flow @@ -476,6 +477,7 @@ public sealed class Resource { * * This can be used to integrate Resources with code which cannot be run within the [use] function. */ + @DelicateCoroutinesApi public suspend fun allocated(): Pair A, suspend (@UnsafeVariance A, ExitCase) -> Unit> = when (this) { is Bind<*, A> -> From 80f5531b63ddaf65da4f6338a7e80e1c0afcf6c3 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Thu, 8 Sep 2022 10:14:22 +0100 Subject: [PATCH 04/10] Update api --- arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api | 1 + 1 file changed, 1 insertion(+) diff --git a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api index 9400aa58ce8..a92511cb23a 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api +++ b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api @@ -303,6 +303,7 @@ public final class arrow/fx/coroutines/Race3Kt { public abstract class arrow/fx/coroutines/Resource { public static final field Companion Larrow/fx/coroutines/Resource$Companion; + public final fun allocated (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun ap (Larrow/fx/coroutines/Resource;)Larrow/fx/coroutines/Resource; public final fun flatMap (Lkotlin/jvm/functions/Function1;)Larrow/fx/coroutines/Resource; public final fun map (Lkotlin/jvm/functions/Function2;)Larrow/fx/coroutines/Resource; From 58720a30ee6c9bef53b9a0f45e8002e47715cfa7 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Thu, 8 Sep 2022 11:44:14 +0100 Subject: [PATCH 05/10] Add constructor ExitCase(e: Throwable) --- .../src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt | 5 +++++ .../src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt | 8 +++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt index d92e4be3343..d9273f3ef95 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Bracket.kt @@ -13,6 +13,11 @@ public sealed class ExitCase { public data class Cancelled(val exception: CancellationException) : ExitCase() public data class Failure(val failure: Throwable) : ExitCase() + + public companion object { + public fun ExitCase(error: Throwable): ExitCase = + if (error is CancellationException) Cancelled(error) else Failure(error) + } } /** diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt index 13d1c9ef0df..8dae17428a1 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt @@ -4,8 +4,8 @@ import arrow.core.continuations.AtomicRef import arrow.core.continuations.update import arrow.core.identity import arrow.core.prependTo +import arrow.fx.coroutines.ExitCase.Companion.ExitCase import arrow.fx.coroutines.continuations.ResourceScope -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable @@ -181,9 +181,8 @@ public sealed class Resource { val a = dsl(effect) f(a) } catch (e: Throwable) { - val ex = if (e is CancellationException) ExitCase.Cancelled(e) else ExitCase.Failure(e) val ee = withContext(NonCancellable) { - effect.finalizers.get().cancelAll(ex, e) ?: e + effect.finalizers.get().cancelAll(ExitCase(e), e) ?: e } throw ee } @@ -497,9 +496,8 @@ public sealed class Resource { } allocate to release } catch (e: Throwable) { - val ex = if (e is CancellationException) ExitCase.Cancelled(e) else ExitCase.Failure(e) val ee = withContext(NonCancellable) { - effect.finalizers.get().cancelAll(ex, e) ?: e + effect.finalizers.get().cancelAll(ExitCase(e), e) ?: e } throw ee } From a2f4dec6c667b085b57b10afb584dcf2d30b8cc8 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Thu, 8 Sep 2022 11:45:01 +0100 Subject: [PATCH 06/10] Improve allocated documentation --- .../kotlin/arrow/fx/coroutines/Resource.kt | 40 +++++++++++++++---- .../kotlin/examples/example-resource-08.kt | 16 +++++--- .../kotlin/examples/example-resource-09.kt | 22 +++------- .../kotlin/examples/example-resource-10.kt | 21 ++++------ .../kotlin/examples/example-resource-11.kt | 4 +- .../kotlin/examples/example-resource-12.kt | 31 ++++++++++++++ 6 files changed, 90 insertions(+), 44 deletions(-) create mode 100644 arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-12.kt diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt index 8dae17428a1..379718246b8 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt @@ -471,10 +471,36 @@ public sealed class Resource { } /** - * Decomposes a [Resource] into a Pair A, suspend (A, ExitCase) -> Unit>, containing - * the allocation and release functions which make up the [Resource]. + * Deconstruct [Resource] into an `acquire` and `release` handlers. + * The `release` action **must** always be called with resource [A] returned from `acquire`, + * if the `release` step is never called, then the resource [A] will leak. The `acquire` and `release` + * steps are already made `NonCancellable` to guarantee correct invocation like `Resource` or `bracketCase`. * - * This can be used to integrate Resources with code which cannot be run within the [use] function. + * ```kotlin + * import arrow.fx.coroutines.* + * + * val resource = Resource({ println("Acquire") }) { _, exitCase -> + * println("Release $exitCase") + * } + * + * suspend fun main(): Unit { + * val (acquire, release) = resource.allocated() + * val a = acquire() + * try { + * /** Do something with A */ + * release(a, ExitCase.Completed) + * } catch(e: Throwable) { + * val e2 = runCatching { release(a, ExitCase(e)) }.exceptionOrNull() + * throw Platform.composeErrors(e, e2) + * } + * } + * ``` + * + * + * This is a **delicate** API. It is easy to accidentally create resource or memory leaks `allocated` is used. + * A `Resource` allocated by `allocated` is not subject to the guarantees that [Resource] makes, + * instead the caller is responsible for correctly invoking the `release` handler at the appropriate time. + * This API is useful for building inter-op APIs between [Resource] and non-suspending code, such as Java libraries. */ @DelicateCoroutinesApi public suspend fun allocated(): Pair A, suspend (@UnsafeVariance A, ExitCase) -> Unit> = @@ -552,7 +578,7 @@ public sealed class Resource { * } * } * ``` - * + * */ public operator fun invoke( acquire: suspend () -> A, @@ -606,7 +632,7 @@ public sealed class Resource { * println(res) * } * ``` - * + * */ @Deprecated( "Use the resource computation DSL instead", @@ -696,7 +722,7 @@ public inline fun Iterable.traverseResource(crossinline f: (A) -> Reso * res.forEach(::println) * } * ``` - * + * */ @OptIn(ExperimentalTypeInference::class) @OverloadResolutionByLambdaReturnType @@ -741,7 +767,7 @@ public inline fun Iterable.traverse(crossinline f: (A) -> Resource) * res.forEach(::println) * } * ``` - * + * */ @Suppress("NOTHING_TO_INLINE") public inline fun Iterable>.sequence(): Resource> = diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt index b1743db3bdf..561d09e3786 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt @@ -3,12 +3,18 @@ package arrow.fx.coroutines.examples.exampleResource08 import arrow.fx.coroutines.* -suspend fun acquireResource(): Int = 42.also { println("Getting expensive resource") } -suspend fun releaseResource(r: Int, exitCase: ExitCase): Unit = println("Releasing expensive resource: $r, exit: $exitCase") +val resource = Resource({ println("Acquire") }) { _, exitCase -> + println("Release $exitCase") +} suspend fun main(): Unit { - val resource = Resource(::acquireResource, ::releaseResource) - resource.use { - println("Expensive resource under use! $it") + val (acquire, release) = resource.allocated() + val a = acquire() + try { + /** Do something with A */ + release(a, ExitCase.Completed) + } catch(e: Throwable) { + val e2 = runCatching { release(a, ExitCase(e)) }.exceptionOrNull() + throw Platform.composeErrors(e, e2) } } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-09.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-09.kt index 7b9020cc4c0..d492b4e97b7 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-09.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-09.kt @@ -3,24 +3,12 @@ package arrow.fx.coroutines.examples.exampleResource09 import arrow.fx.coroutines.* -class File(url: String) { - suspend fun open(): File = this - suspend fun close(): Unit {} - override fun toString(): String = "This file contains some interesting content!" -} - -suspend fun openFile(uri: String): File = File(uri).open() -suspend fun closeFile(file: File): Unit = file.close() -suspend fun fileToString(file: File): String = file.toString() +suspend fun acquireResource(): Int = 42.also { println("Getting expensive resource") } +suspend fun releaseResource(r: Int, exitCase: ExitCase): Unit = println("Releasing expensive resource: $r, exit: $exitCase") suspend fun main(): Unit { - val res = resource { - openFile("data.json") - } release { file -> - closeFile(file) - } use { file -> - fileToString(file) + val resource = Resource(::acquireResource, ::releaseResource) + resource.use { + println("Expensive resource under use! $it") } - - println(res) } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-10.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-10.kt index 57e567884b4..d0bd9907f85 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-10.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-10.kt @@ -14,18 +14,13 @@ suspend fun closeFile(file: File): Unit = file.close() suspend fun fileToString(file: File): String = file.toString() suspend fun main(): Unit { - val res: List = listOf( - "data.json", - "user.json", - "resource.json" - ).traverse { uri -> - resource { - openFile(uri) - } release { file -> - closeFile(file) - } - }.use { files -> - files.map { fileToString(it) } + val res = resource { + openFile("data.json") + } release { file -> + closeFile(file) + } use { file -> + fileToString(file) } - res.forEach(::println) + + println(res) } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-11.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-11.kt index 3987d148522..7a2ed86df12 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-11.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-11.kt @@ -18,13 +18,13 @@ suspend fun main(): Unit { "data.json", "user.json", "resource.json" - ).map { uri -> + ).traverse { uri -> resource { openFile(uri) } release { file -> closeFile(file) } - }.sequence().use { files -> + }.use { files -> files.map { fileToString(it) } } res.forEach(::println) diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-12.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-12.kt new file mode 100644 index 00000000000..ffcb368f565 --- /dev/null +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-12.kt @@ -0,0 +1,31 @@ +// This file was automatically generated from Resource.kt by Knit tool. Do not edit. +package arrow.fx.coroutines.examples.exampleResource12 + +import arrow.fx.coroutines.* + +class File(url: String) { + suspend fun open(): File = this + suspend fun close(): Unit {} + override fun toString(): String = "This file contains some interesting content!" +} + +suspend fun openFile(uri: String): File = File(uri).open() +suspend fun closeFile(file: File): Unit = file.close() +suspend fun fileToString(file: File): String = file.toString() + +suspend fun main(): Unit { + val res: List = listOf( + "data.json", + "user.json", + "resource.json" + ).map { uri -> + resource { + openFile(uri) + } release { file -> + closeFile(file) + } + }.sequence().use { files -> + files.map { fileToString(it) } + } + res.forEach(::println) +} From 1d413dd18027335764f9d0e2a9dbc931ff54aeda Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Thu, 8 Sep 2022 12:17:00 +0100 Subject: [PATCH 07/10] Fix doc examples --- .../src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt | 1 + .../src/jvmTest/kotlin/examples/example-resource-08.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt index 379718246b8..e622f634cf1 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt @@ -478,6 +478,7 @@ public sealed class Resource { * * ```kotlin * import arrow.fx.coroutines.* + * import arrow.fx.coroutines.ExitCase.Companion.ExitCase * * val resource = Resource({ println("Acquire") }) { _, exitCase -> * println("Release $exitCase") diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt index 561d09e3786..fa5813146e6 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt @@ -2,6 +2,7 @@ package arrow.fx.coroutines.examples.exampleResource08 import arrow.fx.coroutines.* +import arrow.fx.coroutines.ExitCase.Companion.ExitCase val resource = Resource({ println("Acquire") }) { _, exitCase -> println("Release $exitCase") From e48a8ecdcec0ac27d5d007acc134dd4720fe51a6 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Thu, 8 Sep 2022 12:17:24 +0100 Subject: [PATCH 08/10] Improve allocated tests --- .../src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt index 004bd250f10..06b8d2d9771 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt @@ -459,11 +459,11 @@ class ResourceTest : ArrowFxSpec( val releaseValue = Random.nextInt() val seed = Random.nextInt() - val (allocate, release) = mkResource({ seed }) { _, _ -> released.complete(releaseValue) }.allocated() + val (allocate, release) = mkResource({ seed }) { i, _ -> released.complete(i) }.allocated() allocate() shouldBe seed - release(1, ExitCase.Completed) + release(releaseValue, ExitCase.Completed) released.getCompleted() shouldBe releaseValue } From 93c37d8e9f597c81d667221eeb64622135fa0d91 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Fri, 9 Sep 2022 10:40:56 +0100 Subject: [PATCH 09/10] Improve testing of allocated --- .../arrow/fx/coroutines/ResourceTest.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt index 06b8d2d9771..6f9ce065375 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt @@ -3,7 +3,9 @@ package arrow.fx.coroutines import arrow.core.Either import arrow.core.identity import arrow.core.left +import arrow.fx.coroutines.ExitCase.Companion.ExitCase import io.kotest.assertions.fail +import io.kotest.inspectors.forAll import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.should import io.kotest.matchers.shouldBe @@ -455,17 +457,20 @@ class ResourceTest : ArrowFxSpec( } suspend fun checkAllocated(mkResource: (() -> Int, (Int, ExitCase) -> Unit) -> Resource) { - val released = CompletableDeferred() - val releaseValue = Random.nextInt() - val seed = Random.nextInt() + listOf( + ExitCase.Completed, + ExitCase.Failure(Exception()), + ExitCase.Cancelled(CancellationException(null)) + ).forAll { exit -> + val released = CompletableDeferred() + val seed = Random.nextInt() - val (allocate, release) = mkResource({ seed }) { i, _ -> released.complete(i) }.allocated() + val (allocate, release) = mkResource({ seed }) { i, _ -> released.complete(i) }.allocated() - allocate() shouldBe seed + release(allocate(), exit) - release(releaseValue, ExitCase.Completed) - - released.getCompleted() shouldBe releaseValue + released.getCompleted() shouldBe seed + } } "allocated - Allocate" { From 61564c9a42b91b30b689ddbe74d63b2d42146db2 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Fri, 9 Sep 2022 11:52:14 +0100 Subject: [PATCH 10/10] Fix compile error --- .../fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api | 5 +++++ .../commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api index a92511cb23a..ac070cde2b0 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api +++ b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api @@ -94,6 +94,7 @@ public final class arrow/fx/coroutines/CircuitBreaker$State$Open : arrow/fx/coro } public abstract class arrow/fx/coroutines/ExitCase { + public static final field Companion Larrow/fx/coroutines/ExitCase$Companion; } public final class arrow/fx/coroutines/ExitCase$Cancelled : arrow/fx/coroutines/ExitCase { @@ -107,6 +108,10 @@ public final class arrow/fx/coroutines/ExitCase$Cancelled : arrow/fx/coroutines/ public fun toString ()Ljava/lang/String; } +public final class arrow/fx/coroutines/ExitCase$Companion { + public final fun ExitCase (Ljava/lang/Throwable;)Larrow/fx/coroutines/ExitCase; +} + public final class arrow/fx/coroutines/ExitCase$Completed : arrow/fx/coroutines/ExitCase { public static final field INSTANCE Larrow/fx/coroutines/ExitCase$Completed; public fun toString ()Ljava/lang/String; diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt index 6f9ce065375..318607a2f6e 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt @@ -460,7 +460,7 @@ class ResourceTest : ArrowFxSpec( listOf( ExitCase.Completed, ExitCase.Failure(Exception()), - ExitCase.Cancelled(CancellationException(null)) + ExitCase.Cancelled(CancellationException(null, null)) ).forAll { exit -> val released = CompletableDeferred() val seed = Random.nextInt()