https://kotlinlang.org logo
#arrow
Title
# arrow
f

Francis Reynders

03/16/2023, 5:03 PM
Hello. I would like to maintain a Resource in an object with some lifecycle. Meaning the resource is acquired during instantiation of the object and should be released when done (an
onExit
method will be called). The
Rescource.use
and
resourceScope
functions only provide the resource in the lambda, however I would like to have the resource available between object initialization and
onExit
, so during the lifecycle of the object). Is there a standard way to do so? I was thinking of creating a CoroutineScope and lauching a coroutine in the
init
part, running resourceScope there and waiting for cancellation. The
onExit
method would cancel that scope. However that would mean the acquired resource would need to be defined as a
lateinit var
rather than val since the compiler does not allow initialization from within the coroutine. It also seems a bit convoluted.
Copy code
class Test() {

  val scope = CoroutineScope(EmptyCoroutineContext)
  lateinit var resource: String

  init {
    scope.launch {
      resourceScope {
        resource = install({"test"}, { _,_ -> Unit })
      }
      awaitCancellation()
    }
  }
If there is a better way I would love to hear about it.
s

simon.vergauwen

03/16/2023, 5:42 PM
Hmm, sounds like you need to bridge worlds? I'm not sure what kind of applications you're building.
however I would like to have the resource available between object initialization and
onExit
with
onExit
do you mean the finalizer of the
Resource
? Or is
onExit
a close/finalizer method of
Test
? The latter is a clear indicator that
Test
is dependent on outer resource. So most commonly used pattern would be to create a smart constructor for
Test
.
Copy code
suspend fun ResourceScope.test(): Test {
  val resource: String = install({"test"}, { _,_ -> Unit })
  Test(resource)
}
Effectively moving the concern "up" closer to
main
or what is typically called the edge of the world. Another option, would be to rely on one of the unsafe constructors
allocate
which turns
Resource
into
Pair<A, suspend (ExitCase) -> Unit>
and turns responsibility back to the user of calling
suspend (ExitCase) -> Unit
. The custom
CoroutineScope
here has the same issue, which is leaking the resource if
cancel
is never called. Note that in the snippet you shared the
resource
is immediately closed, since
awaitCancellation
is called outside of
resourceScope { }
.
f

Francis Reynders

03/16/2023, 5:44 PM
Yes I mean
onExit
would be part of the
Test
class. The reason for this is that the class would encapsulate the lifecycle of some session. Ideally this class would fully hide the fact that the resource is used, taking care of acquiring it and closing it at the end of it's lifecycle. The caller would not be concerned with those details.
s

simon.vergauwen

03/16/2023, 5:46 PM
The custom
CoroutineScope
here has the same issue, which is leaking the resource if
cancel
is never called.
CoroutineScope
is also a
Resource
🙃 https://github.com/47deg/gh-alerts-subscriptions-kotlin/blob/284e5c111b17cff9bc55a9eea2523e298568461b/src/main/kotlin/alerts/KtorUtils.kt#L31
f

Francis Reynders

03/16/2023, 6:06 PM
allocate
does not seem to work in
init
since it's a suspending function
s

simon.vergauwen

03/16/2023, 6:15 PM
That is correct, the
suspend
from
acquire
is moved up to
allocate
. So you have 3 options:
Copy code
suspend fun ResourceScope.test(): Test {
  val resource: String = install({"test"}, { _,_ -> Unit })
  Test(resource)
}
Copy code
suspend fun test(): Test {
  val (res, finalizer) =
    resource({"test"}, { _,_ -> Unit }).allocate()

  // call finalizer on `onExit` also suspend
  Test(resource, finalizer)
}
Copy code
fun test(): Test {
  val resource = resource({"test"}, { _,_ -> Unit })
  val (res, finalizer) = runBlocking { resource.allocate() }

  // call finalizer on `onExit` also suspend
  Test(resource, finalizer)
}
From top to bottom more to less desirable.
use
from Kotlin
Closeable
has the same issue. It's hard to provide better information based on the requirements given 😅 but if you're using
Test
to wrap a single JVM
Closeable
you might be better of not using
Resource
for this use-case.
Resource
truly shines when using larger dependency graphs that are composed of multiple resources, which often need to initialise and close on different dispatchers. And if used in combination with something like
SuspendApp
that ensures back-pressuring your (JVM) application until all resources are closed. This last one is probably the most annoying thing on the JVM with
Closeable
you're responsible of attaching all critical things to
ShutdownHook
etc. For example applications that open
FileHandle
on the OS, but don't close them will result in the OS eventually running out of
FileHandle
and will require a restart of the machine.
f

Francis Reynders

03/16/2023, 6:21 PM
CoroutineScope
is also a
Resource
😀 just shows it's indeed not the best solution. I guess encapsulating side effects is not really FP style. My idea was to fully encapsulate it and capture all errors within the object, so the user would not be concerned with it. I'll need to rethink this. Thanks a lot for your feedback.
s

simon.vergauwen

03/16/2023, 6:24 PM
You meant encapsulating
"test"
and provide an alternative API around it? That can still be done, but then I would opt for option:
suspend fun ResourceScope.test(): Test {
or
fun Test.Companion.create(): Resource<Test>
although that forces the
Resource
on the user. That way you can still wrap the resource "test", and provide an alternative API around it. Which returns
Either
or handles errors accordingly. That is not counter FP in any way. I think the counter FP piece is trying to hide that
Test
is a
Resource
but that leaks out anyway since the user would need to call
onExit
, no?
Copy code
class ExternalRes {
  fun close(): Unit = Thread.sleep(100)
  fun doSomething(): Int = throw RuntimeException("Boom!")
}

class Test(private val res: ExternalRes) {
  // don't swallow but do something proper
  // like send log to metric server
  fun safe(): Int =
    catch({ res.doSomething() }) { -1 }

  companion object {
    fun create(): Resource<Test> = resource {
      val res = install({ ExternalRes() }) { res, _ -> res.close() }
      Test(res)
    } 
  }
}

suspend fun userCode() {
  Test.create().use { test ->
    println(test.doSomething()) // -1
  }

  resourceScope {
    val test = Test.create().bind()
    println(test.doSomething()) // -1
  }
}
f

Francis Reynders

03/16/2023, 6:33 PM
Wow, thank you so much, I see it now. So user is only concerned with the fact that Test is a Resource, that makes sense indeed and rather elegant.
s

simon.vergauwen

03/16/2023, 6:34 PM
My pleasure Francis ☺️
23 Views