Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider adding "finally" #3335

Closed
ncannasse opened this issue Sep 12, 2014 · 32 comments
Closed

Consider adding "finally" #3335

ncannasse opened this issue Sep 12, 2014 · 32 comments
Assignees

Comments

@ncannasse
Copy link
Member

Up to know I was not a big fan of "finally". I find it complexify a bit too much try/catch while not bringing something that can't be done in a more standard way.

However I found that finally allows to have some cleanup code while preserving the full stack trace for the exception (tested in Chrome/JS), which is something right know we only support in Neko with neko.Lib.rethrow.

I wonder if we should either add finally as-it or use it to implement some kind of "rethrow" function on all platforms.

@hexonaut
Copy link
Contributor

+1 I've found myself wanting this construct from time to time.

@waneck
Copy link
Member

waneck commented Sep 12, 2014

That would indeed be awesome. We can allow it on all platforms. For platforms that don't support it natively, it's just a matter of calling / performing a goto to the finally function both inside the catch handler as before any block termination (e.g. return)

@Simn
Copy link
Member

Simn commented Sep 12, 2014

There are some funny cases like finally in a loop that has continue. You have to goto the finally, then back up to the loop condition. Unless I'm misunderstanding how finally works.

@waneck
Copy link
Member

waneck commented Sep 12, 2014

Yes, anything that breaks the flow to out of the try block must go through finally - including break or continue. But it may be a matter of adding the finally function call before each of those statements

@Simn
Copy link
Member

Simn commented Sep 12, 2014

while (cond) {
     try {
          continue;
     }
     finally { }
     otherStuff;
}

I'm pretty sure that should execute the finally block, but not the otherStuff.

@waneck
Copy link
Member

waneck commented Sep 12, 2014

Yes. Here's how I'd transform it:

while(cond) {
  function finallyBlock() {}
  try {
    finallyBlock();
    continue;
  }
  catch(_:Dynamic) {
    finallyBlock();
    throw _; //rethrow when supported
  }
  otherStuff;
}

@Simn
Copy link
Member

Simn commented Sep 12, 2014

What if the finally block has a return? :)

@waneck
Copy link
Member

waneck commented Sep 12, 2014

We could forbid it ;) . I mean - we could deal with that, but it might start to get a little messy.

@waneck
Copy link
Member

waneck commented Sep 12, 2014

Java actually inlines the finally block many times

@Simn
Copy link
Member

Simn commented Sep 12, 2014

I don't know, finally is not really compatible with my control flow aesthetics... :(

@ncannasse
Copy link
Member Author

Yes I don't like it either from a language design point of view, it has too much flows: return is clearly an undefined case for instance, it should even be forbidden at compilation. But what if a throw is made in a catch, the order of finally/catches etc. It's very ambiguous.

I wonder what we could come up that would work better. For instance if you use a "rethrow" in a catch, we use finally to implement it.

@waneck
Copy link
Member

waneck commented Sep 12, 2014

Finally would get called in this case even if no exception is caught

@Simn
Copy link
Member

Simn commented Sep 12, 2014

From what I gather the rule is "if control flow leaves the try block (by whatever means), execute the finally block. If control flow reaches the end of the finally block, continue with what would have originally happened".

@Atry
Copy link
Contributor

Atry commented Sep 12, 2014

finally is a feature that I hate everyday and must-use everyday.

I wonder why finally is came with try and catch. For me, a finally is just some code executed when exiting a scope.

Instead of finally, I consider Scope Exit is what we need.

Scope Exit in other languages:

@hexonaut
Copy link
Contributor

Finally is good for closing resources which is really all I use it for.

//Without finally
var fileHandle = openFile();
try {
    //Do something
    // ...
    fileHandle.close();
} catch (e:Dynamic) {
    fileHandle.close();
    rethrow(e);
}

//With finally
var fileHandle = openFile();
try {
    //Do something
    // ...
} finally {
    fileHandle.close();
}

As an alternative Java has try-with-resources which is even nicer for this case imo. http://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

Using similar to their syntax it could be:

try (fileHandle = openFile()) {
    //Do something
    // ...
}

@Simn
Copy link
Member

Simn commented Sep 12, 2014

Both of these are just narrow version of scope exit...

... which would be even harder to support across all targets, unless you duplicate code like a madman. Which incidentally you can already do with a macro.

@waneck
Copy link
Member

waneck commented Sep 12, 2014

Actually if we forbid return/continue/break inside finally blocks, we can do it on all targets without code duplication.

@Simn
Copy link
Member

Simn commented Sep 12, 2014

At the cost of a closure, which doesn't exactly help with the original stack trace problem I would imagine.

@Atry
Copy link
Contributor

Atry commented Sep 12, 2014

I suggest add a new label syntax:

{
  beforeLabel1();
  label1:
  afterLabel1();
  label2:
  afterLabel2();
}

A label should convert to a ECall that ends at the next }. The above code should equal to:

{
  beforeLabel1();
  label1({
    afterLabel1();
    label2(
      afterLabel2()
    );
  });
}
  • label1 and label2 may be macros, thus we can implement our own Scope Exit in these macro.
  • The label syntax does not introduce any new elements to the untyped AST.

@Atry
Copy link
Contributor

Atry commented Sep 12, 2014

For example, given the code below:

{
  var fd = File.write("file.txt");
  MyMacro.scopeExit(fd.close()):
  fd.writeString("Hello,");
  fd.writeString("World!");
}

, will convert to

{
  var fd = File.write("file.txt");
  MyMacro.scopeExit(fd.close(), {
    fd.writeString("Hello,");
    fd.writeString("World!");
  });
}

@Atry
Copy link
Contributor

Atry commented Sep 12, 2014

The label syntax is a weak version of what @await does.

@Atry
Copy link
Contributor

Atry commented Sep 12, 2014

Or we could use another symbol to avoid ambiguity with ECheckType.

@ncannasse
Copy link
Member Author

My idea is NOT to introduce a new feature or discuss scope exit syntax. I'm perfectly fine with the try/catch as it. We just want a way to keep the stack intact while still performing some exception recovery, which is not possible ATM.

@Atry
Copy link
Contributor

Atry commented Sep 12, 2014

I think we could reuse ... as a placeholder to capture rest expressions.

{
  var fd = File.write("file.txt");
  MyMacro.scopeExit(fd.close(), ...);
  fd.writeString("Hello,");
  fd.writeString("World!");
}

@Atry
Copy link
Contributor

Atry commented Sep 12, 2014

But finally is a half baked solution in comparison to scope exit.

@ncannasse
Copy link
Member Author

Yes, but I don't want either finally or scope exit :)

@Simn
Copy link
Member

Simn commented Jan 15, 2015

I don't know what should happen here.

@ncannasse ncannasse added this to the Long term milestone Jan 15, 2015
@jacekszymanski
Copy link

finally and scope exits serve a very important purpose: to ensure that a thunk of code is executed (entered, in fact) exactly once regardless of how the protected (i.e. try) scope exits. Without this feature I believe it is impossible correctly to write or use anything that relies on reference counters and/or leases (e.g. object pools).

It need not be mixed with try/catch, e.g. Common Lisp has a special operator unwind-protect which only ensures that cleanup code is executed when protected code exits by whatever means, but is not used for error handling.

I don’t think it’s ambiguous, it’s just like @Simn has written above: "if control flow leaves the try block (by whatever means), execute the finally block. If control flow reaches the end of the finally block, continue with what would have originally happened”; and otherwise what would have happened is lost and the non-local exit from finally block is continued.

I agree that finally as it is in Java is a very ugly pattern, but I nevertheless believe that the ability to have a thunk executed exactly once as a cleanup is indispensable. In languages that do not have this ability, but have exceptions, it can be simulated as a code pattern, but this disallows return, continue or break in the protected code with no possibility to enforce this restriction and is thus very error prone.

In its basic form finally does not require code duplication at all, it’s done with a pretty simple rewrite, so what would be

try {
  protected;
}
finally {
  cleanup;
}

becomes

try {
  protected;
  throw PassedOK;
}
catch (e: Dynamic) {
  cleanup;
  if (e != PassedOK) rethrow(e);
}

but, as easily seen, this simple rewrite requires that protected contain no return et al. or else the catch block, and thus cleanup is bypassed.

In Haxe, macros can be used to remove this restriction by means of rewriting return, break and continue to throwing other control exceptions, though to achieve this the ability to expand inside macros is required. I have implemented this in my library scopes, and the only restriction I am now aware of is that the users never throw or catch control exceptions. Once finally (syntax aside) is implemented, scope exit expressions are even easier to add with macros, though still the ability to expand is required.

Adding finally or an equivalent feature would have the advantage that in targets that have finally natively it can be used as is, without throwing unnecessary exceptions, thus making generated code simpler, and in targets that do not have this ability, it can be simulated with exceptions as I described above. It would also make debugging code relying on cleanup expressions way easier, as the macro-generated code is necessarily far from readable.

@ncannasse
Copy link
Member Author

@jacekszymanski one problem with "emulating" finally on platforms that don't support it is that unless they have some way to "rethrow" exceptions this will eat up the callstack, making it very hard to debug such exceptions...

That's one of the main reasons holding me from adding finally as-it in Haxe syntax.

@Danielku15
Copy link

For a start you could add support for finally to the languages that support it natively and also translate it as such:

I understand that Haxe wants to have 100% the same behavior on all platforms and that finally might be implemented differently on some languages. But considering the usage of finally, which is mostly resource-cleanup, it should not be a big deal. If issues are supported from the public, they can be investigated on demand.

The other targets are a bit more tricky:

try {
    <try-body>
}
finally {
    <finally-body>
}

Can become...

{
    ScopeHelper scope([&] () {
        <finally-body>
    });

    <try-body>
}

...where the ScopeHelper destructor calls simply the lambda. This only works for simple cases where finally does not have return statements:

public function test() : Int {
    try {
        return 1;
    }
    finally {
        return 2;
    }
}
var x = test();  // x = 2

In a first version it could be unsupported to have return statements in the finally block to overcome this problem.

Another solution to this topic might be to not support support finally, but only support something like the C# using-block or the Java try-block:

using(<variable-declaration|expression>) <block>
try(variable-declaration|expression>) <block>

Focusing only on some dedicated resource-cleanup control-flows there might be simpler solutions than support any kind of finally-block.

@Sunjammer
Copy link

I know this potentially makes me a filthy hobo, but I really do like returning from a function in the try/catch block set while still having a finally to let me clean up post return but before the stack frame is closed (a trick that felt like discovering time-travel the first time I encountered it). It may be filthy, but it's a very strong productivity feature that makes writing cleaner stateful code much simpler in the scenarios where state can't be avoided.

@ncannasse
Copy link
Member Author

Another thing that should be discussed as part of Haxe Evolution process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants