Olaf Gottschalk
02/22/2023, 7:12 PMResource<A>
and error handling using Either
instead of exceptions. I understood, that resource(aquire, release)
creates a resource that can be used in a resourceScope
- exceptions within acquisition and release are caught and handled. What I currently do not find explained is, what happens on exceptions? Within a resourceScope
block, a resource that is bound using bind function and fails to get constructed makes the complete block finish, including proper release of other resources of that scope. But where and how do I see this? Does resourceScope
rethrow any exception from any resource acquisition inside? And what if all my construction funs are already implementing Either.catch
or Kotlin's Result
techniques for handling exceptions? Do I have to rethrow them in acquisition to make resource work properly?simon.vergauwen
02/22/2023, 7:16 PMDoesYes, just likerethrow any exception from any resource acquisition inside?resourceScope
coroutineScope
it rethrows any thrown exceptions, and it composes any subsequent exceptions that occurred using addSuppressed
such that no information gets lots. I.e.
resourceScope {
val a = install({ }) { _,_ -> throw RuntimeException("A") }
val b = install({ }) { _,_ -> throw RuntimeException("B") }
throw RuntimeException("C")
}
// A ...
// suppressed cause: B .. stacktrace
// suppressed cause: C .. stacktrace
This kind-of depends on the surrounding usage.or Kotlin'sEither.catch
techniques for handling exceptions? Do I have to rethrow them in acquisition to make resource work properly?Result
either<Throwable, A> {
resourceScope {
val a = install({ }) { _,ex -> println("Closing A: $ex") }
Either.catch { throw RuntimeException("Boom!") }.bind()
} // Closing A: ExitCase.Cancelled
} // Either.Left(RuntimeException(Boom!))
A bind
that crosses the resourceScope
will result in release finaliser being called with Cancelled
.
If you switch the order of either
and resourceScope
then it doesn't cancel, or blow up but it closes here with normal state since nothing "failed".
resourceScope {
either<Throwable, A> {
val a = install({ }) { _,ex -> println("Closing A: $ex") }
Either.catch { throw RuntimeException("Boom!") }.bind()
} // Either.Left(RuntimeException(Boom!))
} // Closing A: ExitCase.Completed
Olaf Gottschalk
02/23/2023, 8:22 AMResource.bind()
throws whatever exception is thrown inside the acquisition block... I am actually not seeing any KDoc documentation on this, that's why I was so confused... bind WILL throw...
So, to mitigate this, I need an Either.catch
inside the resourceScope
.
val resource: Resource<String> = resource({
println("Now acquiring...")
error("Nope")
"hi"
}, { s, e -> println("Now releasing $s because of $e") })
resourceScope {
val r = Either.catch { resource.bind() }.getOrElse { error("Caught it : $it") }
println("Using my string resource...")
println(r)
}
or in the context of effects to not use Exceptions
val resource = resource({
println("Now acquiring...")
error("Nope")
"hi"
}, { s, e -> println("Now releasing $s because of $e") })
effect<String, String> {
resourceScope {
val r = Either.catch { resource.bind() }.getOrElse { raise("Caught it : $it") }
println("Using my string resource...")
println(r)
r
}
}
My original question was more regarding a way to avoid dealing with exceptions alltogether. So, the resource methods - they rely completely on exceptions. If my resource acquisition is NOT throwing exceptions, but rather use Raise or Either... how can I use this other than purposely creating an exception?simon.vergauwen
02/23/2023, 8:30 AMResource
has to throw or it can never behave correctly š¤
You can also call raise("failed to initialise resource")
from within the acquire
step if you'd want to, but that pattern is only really valid if you have context receivers.
Or apply this technique:
1. suspend fun ResourceScope.postgres(config: Env.Postgres): Either<PostgresError, NativePostgres>
(source).
2. either { resourceScope { } }
or effect { resourceScope { } }
as you have it there. (source).
Although, you can of-course also do nested monads Resource<Either<PostgresError, NativePostgres>>
and use bind().bind()
but I really dislike that personally.
Is this more in line with what you meant? Or did I misunderstand? š¤Olaf Gottschalk
02/23/2023, 8:46 AMsimon.vergauwen
02/23/2023, 8:49 AMeither { resourceScope { } }
vs resourceScope { either { } }
.Olaf Gottschalk
02/23/2023, 8:50 AMEither
almost everwhere. So I have functions like fun createSapServer(): Either<String, JCoIDocServer>
. This is a resource that needs stopping (closing) at the end of usage.
When now trying to use the cool resource scopes, I struggle because now I am forced to use that function inside a resource definition's acquisition like this:
resource( { createSapServer().getOrElse { error(it) } }, { s, _-> s.stop() })
And even more, now I need to again capture the exception that can be thrown by the resource scope trying to bind this resource... š
My initial thought was, why does the acquire lambda not also have a way to work with Either naturally, without relying on exceptions at all.simon.vergauwen
02/23/2023, 8:52 AMeither {
resourceScope {
install({ createSapServer().bind() }) { s, _
s.stop()
}
}
}
Resource<Either<String, JCoIDocServer>>
Effect
is lazy š¤
fun sapServer(): Effect<String, Resource<JCoIDocServer>> = effect {
resource({ createSapServer().bind() }) { s, _ -> s.stop()}
}
Olaf Gottschalk
02/23/2023, 8:57 AMsimon.vergauwen
02/23/2023, 8:58 AMEitherT[ManagedT[IO, _] String, JCoIDocServer]
.Throwable
or a Raise
has to be seen by the resourceScope
lambda.Olaf Gottschalk
02/23/2023, 9:02 AMinstall
and when would you use resource
?simon.vergauwen
02/23/2023, 9:05 AMflatMap
& co operations could be implemented in a DSL style. While doing so you can avoid all allocations, and typically provide a simpler implementation.
So install
is the DSL version of resource({ }) { _, _ -> }
where Resource<A>
is now a typealias
for suspend ResoureScope.() -> A
.
So when you call resource({ }) { _, _ -> }
it's just calling install
and returning it as a lambda.Olaf Gottschalk
02/23/2023, 9:05 AMfun createSapServer(): Either<String, String> =
"SERVER".right()
//"failed to get server".left()
fun sapServer(): Effect<String, Resource<String>> = effect {
resource({ createSapServer().bind() }) { s, _ -> println("Stopping $s") }
}
val r = either {
resourceScope {
val server = sapServer().bind().bind()
"result coming from $server"
}
}
println(r)
this solution requires triple binds šsimon.vergauwen
02/23/2023, 9:06 AMOlaf Gottschalk
02/23/2023, 9:06 AMfun createSapServer(): Either<String, String> =
"SERVER".right()
//"failed to get server".left()
val r = either {
resourceScope {
val server = install({ createSapServer().bind() }) { s, _ -> println("Stopping $s") }
"result coming from $server"
}
}
println(r)
simon.vergauwen
02/23/2023, 9:07 AM@DslMarker
so you can more easily see the difference between something that "automatically" calls bind
and something that is just a regular value you need to call bind
on.Olaf Gottschalk
02/23/2023, 9:08 AMsimon.vergauwen
02/23/2023, 9:08 AM@DslMarker
somewhere.Olaf Gottschalk
02/23/2023, 9:09 AMsimon.vergauwen
02/23/2023, 9:09 AMOlaf Gottschalk
02/23/2023, 9:09 AMsimon.vergauwen
02/23/2023, 9:09 AMOlaf Gottschalk
02/23/2023, 9:10 AMsimon.vergauwen
02/23/2023, 9:12 AM