Kotlin needs try-with-resources

It’s kind of the same reason that Kotlin doesn’t have lazy as a language feature, or async await as keywords. Kotlin tries to bake in generic features that can then be used to create syntax-y features that aren’t actually part of the language, which basically opens up the opportunity for many different and interesting structurces and paradigms. With higher order functions and contracts, you can perfectly emulate try with resources. Contracts are indeed experimental, but their range is going to be quite wide, that’s why they are experimental. The current version of them works quite well, with a few rough edges. Coroutines were once experimental, but they very quickly became stable. Contracts have got a ton of use cases that would allow you to make more interesting constructs and complex management schemes than just a plain old try with resources or using block that most languages have. Simply, Kotlin is trying to give you a superset of whatever features you need, and then either create by themselves or let the community create a lot of specific features using it. Tbh, I’d prefer more powerful contracts that would let me have the same features that a try with resources or using block would have in other languages cuz then taht also means that I can make more constructs that are specific to my use case (i.e. basically create a flavor of the language depending on whatever domain I am in.) That’s why there’s also a lot of other DSL features that are really generic.

Kotlin is not per se against having it. They’re against implementing specific features and instead opt in for broader features that will give you the same fluency and more. That’s why there are also discussions with the Result type for example about expanding the meaning of the safe navigation operator ?. because it was originally a rather specific features that only deals with nulls, but it could probably be used for wider use cases that guarantee safety and basically communicate the idea of right-biased datatypes (like Eithers and the like)

1 Like

I believe that the minus 100 points (or minus one million $) argument applies only to new ideas. This thing is not new. It is already successfully approved in other languages.

Kotlin does have a foreach operator, for example. Nobody forces us to call .forEach every time we need a loop. How is this any different?

From the same article:

A feature like expression filters in VB is in this bucket. Yes, being able to put a condition on a catch is somewhat more convenient than having to write the test yourself, but it doesn’t really enable you to do anything new.

Expression filters are in this case in other languages. They are tried and tested, yet they didn’t get added in C#

for in kotlin is the way that it is because it allows certain optimizations that wouldn’t otherwise be allowed with foreach. It’s also there to preserve the imperative style way of writing code. for probably cannot be easily replicated with Kotlin’s current features, and if it could then that’s just an oversight due to the fact that these features didn’t exist when for was formalized. If use can be perfectly replicated with no issues in fluency with language features, then there’s no need to add it as a keyword.

Maybe you should reconsider your beliefs.

In some of those questions, the questions asks why we either “took out” or “left out” a specific feature.

The forEach extension is not a good motivation for try-with-resource syntax, IMHO.

Just a small nitpick. I think he’s referring to the for(x in collection) syntax.

forEach does not allow break or continue.

But Kotlin does have break and continue, how is this any different?
This is still irrelevant.

1 Like

@stachenov if you really want it added, I think an effective path for getting it added would need to include discussion on the following aspects of the feature:

  1. Discussion on if the pain point (not having try-with-resources) is either impacting a wide range of use-cases or is highly impactful to a narrow range of use-cases.
  2. Discussion into if there is a deeper root pain point that could be addressed instead (Maybe something that addresses more than this one issue, e.x. non-local break/continue)
  3. Discussion into if there is a library solution or other minimum change solution that would be good enough (Could the problem be solved with just writing it in a less desired way?)
  4. Is there a large-change solution outside of the norm worth developing (let’s not get hyper-focused on one known solution if we could invent something even cooler)
  5. After all those discussions happen, it’s time to evaluate if it’s worth prioritizing over other features that could be worked on. If we don’t get significant gain from the change, or if the pain point is simply not a big or widespread issue, then it’s better to leave it for now.

IMO, these discussions must happen on some level for any feature to get added.


A few decent ideas have been brought up as alternative solutions to try-with-resources that need to be put through their paces:

  1. Using a labeled return
  2. Non-local break & continue
  3. Library / stdlib solution with using scope or ResourceHolder

IMHO, the existing labeled return is good enough for most use-cases. If we include a library solution and non-local break & continue, then odd cases of many resources also become good enough.

It’s fair to still feel that all of the existing solutions are in fact not good enough, but if you want to change it the feature will have to go through the process and overcome some healthy skepticism–it’s not enough to say “I’m familiar with it so we shouldn’t hold back in adding it”.


Maybe it’s worth pivoting the discussion to non-local break and continue instead? It could have the added benefit of solving problems we haven’t discussed unrelated to try-with-resources.
Here’s an example of having non-local continues:

val fileNames = listOf("file1", "mustBeEmpty", "file2")
for (fileName in fileNames) {
    using { // Uses one scope just like try with resources
        val out = Files.newOutputStream(fileSystem.getPath(fileName)).use() // Takes up the same line as the try-with-resources did.
        if (fileName == "mustBeEmpty") continue // Non-local continue
        out.write(byteArrayOf(1, 2, 3))
    }
}

EDIT: If you like the above example and want to use it in your code now you can use labeled returns and it stays pretty clean. One cool spin on this is to remember that you can always use labels and return to perform a break or continue form a lambda based loop.

val fileNames = listOf("file1", "mustBeEmpty", "file2")
fileNames.forEach { fileName ->
    using { // Uses one scope just like try with resources
        val out = Files.newOutputStream(fileSystem.getPath(fileName)).use() // Takes up the same line as the try-with-resources did.
        if (fileName == "mustBeEmpty") return@forEach // Or alternatively return@using
        out.write(byteArrayOf(1, 2, 3))
    }
}
6 Likes

There are multiple problems with use, and there are multiple solutions that may have their own advantages and disadvantages. What’s good about try-with-resources is that it is approved by practice that it is, in fact, a good solution to all of those problems. We may come up with something else, but there’s always the risk that some unexpected inconvenience turns up later.

One especially annoying problem that has nothing to do with local or non-local breaks and continues is how use looks with multiple resources. A very typical example is copying/converting some data from a reader to a writer (file format conversion, for example). With try-with-resources there is no problem. With use I get two nested blocks, double indent for half the price and some extra braces as a free bonus. It isn’t even about semantics. It just looks ugly. In a language that is usually very compact and elegant.

This using approach may solve the multiple resources problem, but it still looks like some kind of black magic. The relationship between using and use() is not immediately visible. They look like completely unrelated constructs. So that’s yet another solution that may more or less work, but still feels inferior to try-with-resources.

You have a point with something looking like magic. Maybe there’s a better wording for using and use (someone suggested autoClose() earlier in this thread).

I feel that the using block is more inline with what a Kotlin programmer would expect. Try-with-resources does not feel like magic to me because I’m familiar. But just like the ternary operator, when excluding my bias, try-with-resources is something that is an odd-ball out when the norm is lambda’s and extension functions.

You make a really good point with handling multiple resources. I don’t like chaining use { ... } and having an extra scope. For me, using fixes this for any number of resources.

I’ll add that although using seems equivalent it’s a bit more flexible. If I want to use multiple resources using try-with-resources I’m forced to assign them at the start of the block. With using I can add them to my cleanup list at any point.
Another difference is conditionally using a resource. With try-with-resources, I must create some temporary variable at the start of the try, or alternatively use multiple try blocks. Here’s an example:

// Try-with-resources
// Notice I must declare all of my cleanup list here or be forced to use multiple try blocks.
try(val out = getOutputStream(), val otherOut = null) {
    if (shouldReadOtherSteam()) {
        otherOut = getOtherStream()
    }
    // ...
}
// Using
using {
    val out = getOutputStream().autoClose() // Switched `use` to `autoClose`
    if (shouldReadOtherSteam()) {
        // Notice how `otherOut` can be limited to this scope.
        val otherOut = getOtherStream().autoClose()
    }
    // ...
}

An alternative example to match the earlier ones. This time we don’t want to cleanup until after we’ve looped through all of the files (probably not what you want to do for file reading but it shows how it’s more flexible than try-with-resources):

val fileNames = listOf("file1", "mustBeEmpty", "file2")
using { // Now we can programmatically add any number of resources to include for auto cleanup!
    fileNames.forEach { fileName ->
        val out = Files.newOutputStream(fileSystem.getPath(fileName)).autoClose()
        if (fileName == "mustBeEmpty") return@forEach
        out.write(byteArrayOf(1, 2, 3))
    }
}
1 Like

Looks like nice improvements over try-with-resources. But how is it supposed to be implemented? using will create some kind of thread-local stack and push created resources onto it? Or will it be a stack of stacks to allow for nested using?

To me, stream.autoClose() sounds like I’m closing immediately. To assume otherwise requires additional knowlege or surrounding scope (using).

Go language has defer statement which has the effect of executing given function after the surrounding function returns. It is used like this:

f, err := os.Open(name)
defer f.Close()

Maybe the better name for autoClose is deferClose.

Here’s a simple implementation (Be sure to try running and editing it right in the forum!):

class ResourceScope {
    private val resources = mutableListOf<AutoCloseable>()
    
    fun close() {
        // In this case I chose to close with first-in-first-out.
        // You could always extend this class to enable other kinds of closing strategies.
        resources.forEach { it.close() } 
    }
    
    fun AutoCloseable.autoClose() { 
        resources += this 
    }
}

fun using(block: ResourceScope.() -> Unit) {
    val rs = ResourceScope()
    try {
        rs.block()
    } finally {
        rs.close()
    }
}

fun main() {
//sampleStart
    using {
        val myResource = ExampleClosable("myResource").autoClose()
        for (i in 0..5) {
            ExampleClosable("$i").autoClose()
        }
        // error("Triggered error!") // Try uncommenting this to see that no resources are left un-closed.
        println("Success!")
    }
//sampleEnd
}

class ExampleClosable(val name: String) : AutoCloseable {
    init {
        println("Created resource $name")
    }
    override fun close() = println("Closing $name")
}

I’m sure there will be plenty of ways to improve on it. Like mentioned earlier in this thread, the ResourceScope (others used ResourceHolder) could be configured to handle resources in different ways allowing interesting solutions.

Well, that’s exactly why I’m against using library functions for what should have been language constructs. It looks like a language construct, but behaves slightly different, which can be pretty confusing at times.

In this case, it obviously introduces an invisible this into the scope, which can be annoying and confusing. It can be fixed by replacing this with a thread-local variable, and it would also speed up things a bit because the resource collection can be reused. Not sure if that doesn’t bring any other confusion into the picture…

And this one has a very subtle bug: by using a list instead of a stack, it closes the resources in the same order they were opened. This is especially annoying because it doesn’t matter in most cases, and yet it is completely counter-intuitive. This could be easily fixed by using a stack, though.

Off topic: this forum needs hands-on help. I couldn’t figure out how to post code like that and how to edit the greyed-out parts of it (which would be needed if I wanted to demonstrate the this problem).

My example had the resources close in FIFO order to show how you could design different closing strategies but I originally wrote it in LIFO order to match exactly how try-with-resources behaves. Luckily since it’s a library function and not a built in language construct we can easily change it’s behavior.

I think using a library function is idiomatic; it’s expected and the norm for a Kotlin programmer.
Switching the this inside a function scope is very common. It’s how all of Kotlin’s typesafe DSLs, most functional APIs, coroutines, and more, work.

Here’s my understanding of the main cons for a library alternative to a try-with-resources language concept–let me know if I’m misunderstanding:

Cons:

  1. It can behave differently than expected (different from try-with-resources)
  2. Its flexibility can be hidden and cause confusing changes in behavior
  3. It switches the receiver, this
  4. Library functions are slower
  5. try-with-resources is the norm and extension functions are magic

And here’s another way of viewing those:

Pros:

  1. It can behave exactly as expected to perfectly match try-with-resources
  2. It can force an explicit behavior selection so you never have to wonder what it will do (I didn’t show this in my example but it can be done)
  3. It switches the receiver, this, which matches what a Kotlin programmer is comfortable with and expects
  4. Library functions are not slower
  5. try-with-resources is magic and extension functions are the norm

I see how the cons could fit in another language but IMO I think the pros are a better description of the situation for Kotlin.

EDIT: You mention the this problem. I think you have a point that nested receivers can at times get cumbersome but that’s a problem shared by the even more common use-cases in Kotlin. There might be a more elegant way of solving nested receivers for all cases; I don’t think solving it for this one specific case will make much of a difference

EDIT EDIT: Just to clarify, I do think TwR is beneficial. Just that the dev/compiler cost to add a small good thing often isn’t small. Personally I’d prioritize something like multi-catch before TwR (and other features like klib and web assembly over those). At least with TwR we can create an equivalent and possibly superior alternative ourselves.

4 Likes

5 posts were split to a new topic: How to post a runnable code block on this forum