I must admit, it's pretty cool.
But to provide some constructive criticism:
* This code is not 100% safe. In particular, adding to a list is not safe from exceptions (OutOfMemoryError), so you’ll get into a situation where you lose track of a resource and don’t close it. If you use a fixed-length array, you’ll run out of room.
* You’ll still often need a try/catch block
* This syntax is still a little ad-hoc (much better, yes, but you still have to remember to add “.autoClose()”)
Why do you say “we want to avoid language construct just for this as much as possible”?
The point of language constructs (at least the really good language constructs!) is to cement a “pattern” from being some ad-hoc boilerplate code into a syntax that jumps out at the user and tells them to “use it correctly!” Gotos became if/for/while, structs with function pointers became objects, continuations became exceptions and async/await, etc, etc. Yet, there’s not many code patterns more fitting of a language construct than correct resource lifetime management. Java and C# both fucked up in not even shipping Closeable/IDisposable in versions 1. (The hubris!) Then they fucked up in how they implemented Closeable/IDisposable. (Java had to release another class, AutoCloseable, and C# requires developers to read giant articles like http://www.codeproject.com/Articles/15360/Implementing-IDisposable-and-the-Dispose-Pattern-P to even write a class that implements IDisposeable. Microsoft actually “fixed” it again when it created managed Visual C++ by generating the boilerplate automatically https://msdn.microsoft.com/en-us/library/ms177197%28v=vs.100%29.aspx , which is one of the very few advantages of Visual C++ as a managed language. Yes, the irony.)
Don’t let this happen to Kotlin! As this author http://www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-About pointedly states, “Never design a language with exceptions that do not also have deterministic resource deallocation.”
So, I would advise you to consider this subject seriously, and support the user with solid language constructs to deal with resource lifetime management. Even if we just make your “using” function into a keyword, we’re cementing it with syntax highlighting, and prohibing people overriding, overloading, or otherwise messing with the semantics. I’m sure there’s plenty of other things that could be tidied up along the way. I’m afraid I don’t have a ready list, but a pain point that comes to mind is implementing close
for a class that has multiple autocloseable fields (which it owns):
class MyClass : AutoCloseable
{
Closeable a = new MyCloseable();
Closeable b = new MyCloseable();
Set<Closeable> c = …;
@Override public void close() throws Exception // Really icky. But we can’t just throw IOException, since the type of ‘t’ is unrestricted.
{
Throwable t = null;
try { if (a != null) a.close() } catch (Throwable t2) { if (t==null) t = t2; else t.addSuppressed (t2); } // We want to catch Errors as well, since there’s no reason to not try to dispose of our resources if there’s been an Error (the Error may be caught and the program may continue).
try { if (b != null) b.close() } catch (Throwable t2) { if (t==null) t = t2; else t.addSuppressed (t2); } // A trickier case is the InterruptedException, but I think we should catch those as well (since interrupting a thread is not guaranteed to work)
for (Closeable cc : c) try { if (c != null) c.close() } catch (Throwable t2) { if (t==null) t = t2; else t.addSuppressed (t2); } // Oh, geez, do we need to put for
inside a try block as well??
if (t != null)
throw t; // I wonder, if this exception also gets suppressed, will the suppressed exceptions in this exception still get printed? Or will we need to “unroll” them?
}
}
Maybe we can have ‘autoclosed var’ and ‘autoclosed val’ which will close the object whenever it leaves the scope? No “using” keyword required. For fields/properties, ‘autoclosed’ generates a ‘close’ method. This should be enough for most AutoCloseable classes (which just need to close their fields). For classes with more complex close logic, the user can specify one or more “destructors” which will be called from the generated close method using the appropriate try { } catch { suppress } logic. Additional features would be correct chaining of ‘close’ methods in the inheritance heirarchy, calling any finalizer logic from ‘close’ (and making any class which has a finalizer AutoCloseable), checking if the object has been closed on every method call (or at least maintain an isClosed property which can be manually checked), not running the destructors/finalizers more than once, etc. Maybe even a ‘close’ keyword for use inside a destructor (to catch the exceptions, as above). Basically, take a look at what Visual C++ does on the CLR.
closeable class MyClass
{
autoclosed val a = MyCloseable();
autoclosed val b = MyCloseable();
var c : Set<Closeable> = …;
destructor ()
{
for (val cc in c)
close cc; // Gotta close 'em all!
}
}
To go the final mile, we can also re-implement finalization according to this advice http://www.oracle.com/technetwork/java/javamail/finalization-137655.html in order to mimic CLR’s GC.SuppressFinalize to avoid the finalization performance penalty http://lukequinane.blogspot.com/2009/03/java-suppress-finalizer.html if close has already released all resources.
The article I linked to previously (http://www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-About) highlights other big-picture issues with the dispose pattern, such as having to pre-determine object ownership and how it really fucks with the Liskov Substitution Principle in that if you have an object which implements interface Foo which does not derive from AutoCloseable, you don’t actually know whether the object itself implements AutoCloseable and needs to be closed (!!!). However, the proposed solution, reference counting, seems too radical for Kotlin. The article also brings up a laundry list of specific issues in the CLR, but I haven’t parsed it to figure out which apply to the JVM. (Generally, the JVM has much fewer such issues.)
Btw, you know what would be a quick fix to point #2 and a great addition to the language in general? Allowing catch/finally blocks to follow any block, permitting code like:
using {
val input = Files.newInputStream(from).autoClose()
val output = Files.newOutputStream(to).autoClose()
input.copyTo(output)
}
catch (IOException e) {
;
}
as well as:
fun foo() {
…
}
catch (…) {
// look ma! no nested scopes!
}
finally {
// let’s test postconditions
}
“try” is no longer a keyword