dart-lang / language

Design of the Dart language
Other
2.67k stars 205 forks source link

Add a "try-with-resources" or "using" style syntax for automatic resource management #2051

Open jimsimon opened 6 years ago

jimsimon commented 6 years ago

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:

try (CloseableResource cr = new CloseableResource) {
  ...
} //optionally add a catch and/or finally branch here

and in C# with the IDisposable and using syntax:

using (IDisposableResource idr = new IDisposableResource()) {
  ...
}

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

matanlurey commented 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());
}
jimsimon commented 6 years ago

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.

eernstg commented 6 years ago

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.

feinstein commented 5 years ago

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.

kevmoo commented 5 years ago

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.

feinstein commented 5 years ago

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?

feinstein commented 5 years ago

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.

munificent commented 5 years ago

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.

jimsimon commented 5 years ago

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()
) {
  ...
}
AndreTheHunter commented 2 years ago

Having a standard Disposable interface https://github.com/dart-lang/sdk/issues/43490 would enable this to be standard.

MarvinHannott commented 5 months 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());
}

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.

lrhn commented 5 months ago

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) {
  // ...
}));
MarvinHannott commented 5 months ago

@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.

lrhn commented 5 months ago

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.