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 .. stacktracesimon.vergauwen
02/22/2023, 7:19 PMThis kind-of depends on the surrounding usage.or Kotlin'sEither.catchtechniques 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.Completedsimon.vergauwen
02/22/2023, 7:23 PMOlaf 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()
}
}
}simon.vergauwen
02/23/2023, 8:53 AMResource<Either<String, JCoIDocServer>>simon.vergauwen
02/23/2023, 8:55 AMEffect 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].simon.vergauwen
02/23/2023, 9:01 AMThrowable 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?Olaf Gottschalk
02/23/2023, 9:04 AMsimon.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