Here's a question regarding `Resource<A>` an...
# arrow
o
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
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.
Copy code
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.
Copy code
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".
Copy code
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
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
.
Copy code
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
Copy code
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
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
Yeah, I think we are on the same page.
s
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
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
It should work like this, no?
Copy code
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 ๐Ÿค”
Copy code
fun sapServer(): Effect<String, Resource<JCoIDocServer>> = effect {
  resource({ createSapServer().bind() }) { s, _ -> s.stop()}
}
o
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
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
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
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
Copy code
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
"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
while this one only one
Copy code
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
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
if you look at the double bind().bind() solution, only the second bind gets colored though.
s
Oh, then that one is missing
@DslMarker
somewhere.
o
Correct. The bind() in Raise.kt does not have it
s
Thanks for reporting!! ๐Ÿ™
o
PR for me? ๐Ÿ™‚
s
Yes, that'd be awesome!
o
Do I need an issue first or is this not required?
s
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 โ˜บ๏ธ