Open jimsimon opened 6 years ago
You could easily accomplish this with closures in Dart as-is:
abstract class Resource {
void dispose();
}
void using<T extends Resource>(T resource, void Function(T) fn) {
fn(resource);
resource.dispose();
}
Example use:
main() {
using(new FileResource(), (file) {
// Use 'file'
});
}
It's unlikely this will be added in the language, since unlike Java there is a good way to create your own syntaxes/DSLs with closures today. For some examples, you could look at package:pool
, which is sort of built around this idea:
// Create a Pool that will only allocate 10 resources at once. After 30 seconds
// of inactivity with all resources checked out, the pool will throw an error.
final pool = new Pool(10, timeout: new Duration(seconds: 30));
Future<String> readFile(String path) {
// Since the call to [File.readAsString] is within [withResource], no more
// than ten files will be open at once.
return pool.withResource(() => new File(path).readAsString());
}
For what it's worth, your example is doable in Java/C# if the using
function existed on a class (it could even be a static function). The main reason for making it a part of the language is that it allows tools (in Dart's case the analyzer) to ensure that a resource is properly closed and warn the developer if it isn't. This is extremely useful for large-scale projects, especially ones with entry level engineers on it, and the "build your own" approach doesn't allow for it. Alternatively, the analyzer could warn when any class with a close function never has it called, but without the contractual/spec obligation it would only be guessing. I also truly think this feature falls in line with three of Dart's core goals: Provide a solid foundation of libraries and tools
, Make common programming tasks easy
, and Be the stable, pragmatic solution for real apps
. I really hope you reconsider adding this feature. It's not a "must-have", but it's definitely a "nice-to-have" that can help the language stand out a little more and provide some nice polish.
I can see that Matan's approach will do almost exactly what was requested in this issue, but I still have this nagging suspicion that there could be more to resource finalization than this, and this issue could serve to clarify the situation. With that in mind, I'll reopen this issue and mark it with new labels. Of course, there is no guarantee that we will accept such a language enhancement proposal in the end.
I am new to Dart, but I am experienced in C# and in Java, and as far as I can see @matanlurey example will not be the same as in Java's try with resources
or C#'s using
.
It all boils down to Exception
handling. If an Exception
is thrown inside Matan's using()
method, that Resource
will never be disposed. Whereas in Java and in C#, the language surrounds everything with a try-finally
so no matter what happened inside the execution block, the Resource
is disposed at the finally
block added by the language.
Java also takes care to not suppress any Exception
thrown inside the finally
block.
I think a feature like this is very welcome to the language, since it makes coding simpler and less error prone to forget to close/dispose a resource, specially on edge cases where Exception
may arise inside a try
and/or a finally
block, like with database and network connections.
EDIT:
I found this autoClose
method, it solves this (although I don't think the error handling will be as elegant as in a try with resources
from Java), I just don't get why there's a null check for the autoCloseable
and not one for the action
.
To put code in @matanlurey's mouth
void using<T extends Resource>(T resource, void Function(T) fn) {
try {
fn(resource);
} finally {
resource.dispose();
}
}
The issue with this approach: you pay for a closure which could be avoided it this was a language feature.
Yes, and I am not experienced enough in Dart to state safely what's going to happen if resource.dispose()
throws an Exception
itself.
I believe that even if you make a catch()
and pass an onError
closure, as autoClose
does, we would also have to surround this whole code with a try-catch
, only to get any Exception
thrown inside the finally
clause, making this whole code very cumbersome and prone to error...am I wrong?
If this is the case, then having a try with resources
that catch
Exception
thrown inside the creation of the Resource
, the execution of the computation or the closing of the Resource
inside a finally
could make things a lot simpler and safer.
If we were to add this to the language, I think we should do something a little more flexible than C# and Java and have it scoped to the lifetime of a variable. When you have multiple resources that need to be guarded, you get a pyramid of braces, which gets unwieldy:
using (var a = new DisposableA()) {
using (var b = new DisposableB()) {
using (var c = new DisposableC()) {
...
}
}
}
C# lets you omit the braces, which helps when you have sequential resources:
using (var a = new DisposableA())
using (var b = new DisposableB())
using (var c = new DisposableC()) {
...
}
But it falls apart if you need to do anything between those using statements:
using (var a = new DisposableA())
if (a.Something()) return;
using (var b = new DisposableB())
if (b.AnotherThing()) return;
using (var c = new DisposableC()) {
...
}
This either doesn't compile or, if it does, doesn't do what you want. If we're going to do something analogous in Dart, I think we should jump right ahead to what C# is adding with using
on variable declarations:
using var a = new DisposableA();
if (a.Something()) return;
using var b = new DisposableB();
if (b.AnotherThing()) return;
using var c = new DisposableC();
...
With that, the resource is disposed when the variable goes out of scope. I think it's a much cleaner, more expressive syntax and follows C++'s RAII thing. I don't have a proposal for what syntax we should use for Dart , but something roughly analogous should work.
It's been awhile since I've written any Java, but IIRC you can put multiple Closeable resources in the same try block and they will be closed in reverse order.
try (
CloseableA a = new CloseableA();
CloseableB b = new CloseableB();
CloseableC c = new CloseableC()
) {
...
}
Having a standard Disposable interface https://github.com/dart-lang/sdk/issues/43490 would enable this to be standard.
You could easily accomplish this with closures in Dart as-is:
abstract class Resource { void dispose(); } void using<T extends Resource>(T resource, void Function(T) fn) { fn(resource); resource.dispose(); }
Example use:
main() { using(new FileResource(), (file) { // Use 'file' }); }
It's unlikely this will be added in the language, since unlike Java there is a good way to create your own syntaxes/DSLs with closures today. For some examples, you could look at
package:pool
, which is sort of built around this idea:// Create a Pool that will only allocate 10 resources at once. After 30 seconds // of inactivity with all resources checked out, the pool will throw an error. final pool = new Pool(10, timeout: new Duration(seconds: 30)); Future<String> readFile(String path) { // Since the call to [File.readAsString] is within [withResource], no more // than ten files will be open at once. return pool.withResource(() => new File(path).readAsString()); }
The only reason this needs to be a language feature is (of course 😉) (async) Exceptions. The C++ standard guarantees that even if a destructor throws an Exception, following destructors will still run, and std::terminate is called when a destructor throws during stack unwinding, so not to leave the program in an undefined/wrong state. It's not entirely impossible to implement such behavior in normal Dart code today, I think, though I wouldn't know how to handle Records properly (which weren't even a thing back when you wrote your reply), and it is easy to get it wrong. There are just so many corner cases. So I think this should be handled by the Dart team, and I am really perplexed they haven't dealt with this yet. Don't get me wrong, I love all the new Dart features, but sometimes the Dart team appears a bit unfocused, implementing all sorts of cool stuff while avoiding the really important things like resource handling or intersection types.
Full C++ RAII is a very different beast from "try-with-resource" or "using". I don't think we're going to aim for C++ RAII.
A "using" feature would know precisely which objects are to be disposed, because they are the values of specific expressions which must implement some kinds of Disposable
/Cancelable
interface. You can't dispose a record, it doesn't implement Cancelable
.
So:
using {
var db = await connectToDatabase();
var tr = await db.startTransaction(); // Implements `AsyncCancelable`, its `Future<void> cancel()` rolls back the transaction.
} try {
var response = await tr.query("SELECT * FROM bananas");
}
which would invoke tr.cancel()
(and await it), then db.cancel()
, and then complete with the last error among those (or all the errors in one error object), and otherwise normally.
This is something you can do using code today, you just need to do one resource at a time, which can be annoying.
void using<T extends Cancelable>(T resource, void Function(T) body) {
try {
body(resource);
} finally {
resource.cancel();
}
}
Future<void> usingAsync<T extends Cancelable>(T resource, FutureOr<void> Function(T) body) async {
try {
await body(resource);
} finally {
await resource.cancel();
}
}
and then:
await usingAsync(connectToDataBase(), (DB db) => usingAsync(db.startTransaction(), (Transaction tr) {
// ...
}));
@lrhn
Full C++ RAII is a very different beast from "try-with-resource" or "using". I don't think we're going to aim for C++ RAII.
Sorry, I didn't want to suggest you guys should go full C++. In fact, I wasn't suggesting any concrete solution! I was just trying to demonstrate why Exceptions are so important to deal with when it comes to resource management, and why I think the C++ standard got it right.
This is something you can do using code today, you just need to do one resource at a time, which can be annoying.
Okay, but how would a bunch of Disposables interact with each other when they all live in their own scope? I mean, that is the exact problem we are trying to solve: multiple Disposables live in the same scope and either construction as well as destruction could fail with an Exception, leaving the other Disposables bad. That's the sole reason I spoke about C++.
You can't dispose a record, it doesn't implement Cancelable.
But members of it could. And when a function constructs multiple Disposables (and maybe some additional non-Disposables for that matter) and returns them as record, or even as record of records, we still need to deal with failed construction as well as failed destruction.
If I had to present a solution, I would absolutely go for D style Scope Guards. It's the most general, expressive, elegant, and flexible solution I could think of. And that is definitely not something that could be implemented in pure Dart today.
how would a bunch of Disposables interact with each other when they all live in their own scope?
Scope nesting works for that.
If a scope is entered, then allocation has succeeded. The finally-block calling the cancel
will be run when exciting the scope, no matter what happened before.
If cancelling fails, then it throws, but nesting finally blocks will still run. At worst, some errors are lost, but that's why you shouldn't do multiple things that can fail at the same time.
One neat feature in Java and C# that I think would be great for Dart is the ability to have a resource be automatically closeable/disposable. This is accomplished in Java with the Closeable and/or AutoCloseable interface paired with the following try-catch syntax:
and in C# with the IDisposable and using syntax:
This helps catch resource leaks because the compiler can detect and warn the developer when close/dispose has not been called or has been called unsafely. I'd love to see this feature in Dart!
Edits: Spelling and grammar fixes