https://kotlinlang.org logo
Title
o

Olaf Gottschalk

02/22/2023, 7:12 PM
Here's a question regarding
Resource<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?
s

simon.vergauwen

02/22/2023, 7:16 PM
Does
resourceScope
rethrow any exception from any resource acquisition inside?
Yes, just like
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
Either.catch
or Kotlin's
Result
techniques for handling exceptions? Do I have to rethrow them in acquisition to make resource work properly?
This kind-of depends on the surrounding usage.
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
So they naturally compose in a principled way how you would otherwise expect from functional effect systems using monad transformers etc
o

Olaf Gottschalk

02/23/2023, 8:22 AM
Ok, so `ResourceScope`'s
Resource.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?
s

simon.vergauwen

02/23/2023, 8:30 AM
I think this is not explicitly mentioned in the docs like this because
Resource
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? 🤔
o

Olaf Gottschalk

02/23/2023, 8:46 AM
Yeah, I think we are on the same page.
s

simon.vergauwen

02/23/2023, 8:49 AM
It's high on my list to document better how this different patterns/DSL work with each-other. It's mostly about ordering of
either { resourceScope { } }
vs
resourceScope { either { } }
.
o

Olaf Gottschalk

02/23/2023, 8:50 AM
My problems come from me formerly trying to catch exceptions as soon as possible and to transform them to
Either
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.
s

simon.vergauwen

02/23/2023, 8:52 AM
It should work like this, no?
either {
  resourceScope {
    install({ createSapServer().bind() }) { s, _
      s.stop()
    }
  }
}
You can however not do
Resource<Either<String, JCoIDocServer>>
I should try it myself, but this might work. Since
Effect
is lazy 🤔
fun sapServer(): Effect<String, Resource<JCoIDocServer>> = effect {
  resource({ createSapServer().bind() }) { s, _ -> s.stop()}
}
o

Olaf Gottschalk

02/23/2023, 8:57 AM
I am trying all of this right now... it just makes me worry this makes my head spin so fast right now, even though I want things to be easy to reason about. 🙂 That's why I love Arrow after all!
s

simon.vergauwen

02/23/2023, 8:58 AM
It's the equivalent of monad transformers, so in comparison I think this is still an "easy" pattern to reason about. In Scala this would look something like
EitherT[ManagedT[IO, _] String, JCoIDocServer]
.
The only thing that is important to remember here is to close the resources a
Throwable
or a
Raise
has to be seen by the
resourceScope
lambda.
o

Olaf Gottschalk

02/23/2023, 9:02 AM
So, when would you use
install
and when would you use
resource
?
BTW I noticed the Resource DSL uses DslMarkers while the Raise DSL does not. One gets colored by IntelliJ, the other isn't...
s

simon.vergauwen

02/23/2023, 9:05 AM
When designing Arrow 2.x we realised that all the
flatMap
& 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.
o

Olaf Gottschalk

02/23/2023, 9:05 AM
fun 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 😉
s

simon.vergauwen

02/23/2023, 9:06 AM
"All problems are solved by adding 1 layer of indirection", this is one of the things that remained true for me most of my career so far 😁
o

Olaf Gottschalk

02/23/2023, 9:06 AM
while this one only one
fun 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)
s

simon.vergauwen

02/23/2023, 9:07 AM
Only DSL functions are marked with
@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.
o

Olaf Gottschalk

02/23/2023, 9:08 AM
if you look at the double bind().bind() solution, only the second bind gets colored though.
s

simon.vergauwen

02/23/2023, 9:08 AM
Oh, then that one is missing
@DslMarker
somewhere.
o

Olaf Gottschalk

02/23/2023, 9:09 AM
Correct. The bind() in Raise.kt does not have it
s

simon.vergauwen

02/23/2023, 9:09 AM
Thanks for reporting!! 🙏
o

Olaf Gottschalk

02/23/2023, 9:09 AM
PR for me? 🙂
s

simon.vergauwen

02/23/2023, 9:09 AM
Yes, that'd be awesome!
o

Olaf Gottschalk

02/23/2023, 9:10 AM
Do I need an issue first or is this not required?
s

simon.vergauwen

02/23/2023, 9:12 AM
Whatever you prefer is fine. Since you found it, I doubt anyone else will pick it up without an issue being there. So creating a PR reporting a bug attached with the relevant fix immediately is always loved by maintainers ☺️