-
Notifications
You must be signed in to change notification settings - Fork 59
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
Auto Closing #119
base: master
Are you sure you want to change the base?
Auto Closing #119
Conversation
Fixed it. |
Requiring a method specifically called What about Go-style defer: final foo = new Bar();
defer foo.destroy();
mutex.lock();
defer mutex.unlock(); Pros: not restricted to close method, can be used for more than just cleaning up resources Python's with statement is also nice. with(final foo = new Bar()) {
}
with(mutex) {
} Pros: same pros as defer + more clearly defined scope and potential for implementers to have custom handling in case of exceptions (eg a database could roll back a transaction) |
Not seen that go one before, I assume that I did think about also adding that |
I've added an extra section to unresolved questions about types which don't have a close function but would benefit from this sort of transformation (a lot of stuff in the threading packge), maybe syntax like Also added a part in opening possibilities about extra syntax similar to Got carried away on the weekend had a go at making a proper implementation. I've got a new filter which applies the same sort of transformation as the linked macro but to the texpr. Handles classes, abstracts, forwarding, inheritance, etc, also checks the signature of found close functions to ensure they're suitable. https://github.com/Aidan63/haxe/tree/autoclose With that branch you can see nice code such as this. class Foo {
public function new() {}
public function close() {
trace('foo');
}
}
abstract Bar(String) {
public function new() {
this = "";
}
public function close() {
trace('bar');
}
}
@:forward(close) abstract Baz(Bar) {
public function new() {
this = new Bar();
}
}
function main() {
autoclose final writer = sys.io.File.write("test.txt", false);
autoclose final bar = new Bar();
for (_ in 0...Std.random(10))
{
autoclose final foo = new Foo();
}
autoclose final baz = new Baz();
} get transformed into nightmarish try catches looking like this. private class _Main.Main_Fields_ {
@:keep
public static function main() {
var writer = sys.io.File.write("test.txt", false);
try {
var bar = _Main.Bar_Impl_._new();
try {
{
var _g = 0;
var _g1 = Std.random(10);
while ((_g < _g1)) {
var _ = _g ++;
var foo = new Foo();
try {} catch (_hx_exn:Any) {
if ((foo != null)) foo.close();
throw _hx_exn;
};
if ((foo != null)) foo.close();
};
};
var baz = _Main.Baz_Impl_._new();
try {} catch (_hx_exn:Any) {
if ((baz != null)) _Main.Bar_Impl_.close(baz);
throw _hx_exn;
};
if ((baz != null)) _Main.Bar_Impl_.close(baz);
} catch (_hx_exn:Any) {
if ((bar != null)) _Main.Bar_Impl_.close(bar);
throw _hx_exn;
};
if ((bar != null)) _Main.Bar_Impl_.close(bar);
} catch (_hx_exn:Any) {
if ((writer != null)) writer.close();
throw _hx_exn;
};
if ((writer != null)) writer.close();
}
} Consecutive autoclose variables could be reduced down to a single try catch but are not at the moment. |
+1 to "defer ...", this is helpful for many cases - and not limited to classes (with very specific requirements) |
Defer would be a superb way to tackle this and also related issues, like the absence of |
Putting aide my dislike of the defer for a minute (reading the go docs certaintly didn't help, I hadn't even though of cases where the defered function might accept arguments...), I think a big advantage of restricting to a specific function or interface is that it assists with self documenting code and opens up tooling possibilities. Restricting to something like In the C# world visual studio will run analysers to detect if a Areas of the standard library where autoclose / with would be useful but close is not used (such as the mutex case mentioned above), it seems like it would be pretty easy to add new functions which return wrapper abstracts or similar which provide a close function. |
To be honest the idea of having "single method for cleaning up resources" is controversial. HAXE already have established the grounds here - there is no examples of fixed naming of class methods, compiler keywords are the only place with "restrictive" naming so far. Partially this is due keywords can be translated into different implementations for different targets - while class methods are locked to have same name in every target (can be accessed via reflection, etc). Some other considerations:
Haxe already used as a bridge language to other local domain frameworks. So flexibility and non-restrictive implementation is the key of wide adoption, imho |
That's an ambitious goal, especially in light of the fact that Haxe's ecosystem includes the ecosystems of all the compiler targets. And why of all things call it var something = pool.alloc();
something.thatThrows();
pool.release(something) Here the object/resource that one wants disposed cannot dispose itself. You raise one good point: having some explicit way to denote objects as requiring manual cleanup by the developer, so that the compiler/IDE can detect failure to do so. I would probably suggest having something like |
There are plenty of places in haxe where certain syntax features can only be used if certain functions are provided. For loops can only be used with types which unify with any of the iterator / iterable structures, they're the reason why I proposed anything which provides a
It's a "single recommended way of cleaning up resources that provides guarentees arounds execution", library authors are free to continue to use whatever functions they want and users can still attempt to clean up manually, but providing quality of life features around a single method means people are probably going to coverge to using it over time.
Keyword naming has nothing to do with target languages nor are function names locked, most generators maintain their own list of target specific keywords and will mangle any haxe code which conflicts with this. This also works fine with reflection, I can create a field called Your other comments make me thing you haven't quite grasped whats going on here or even read the proposal. I have not suggested it be restricted to a class, anything that provides a
I don't know what you mean by this, but again I don't think you quite understand whats happening, all this proposal does is generate a series of try catches and adds those close calls where ever the execution scope ends. I've posted a macro with this proposal which does exactly this and I'm not aware of any target specific problems. The generated code would also assumably be very similar if defer was implemented, it would still generate the try catches and insert the calls where ever ever the execution scope ends.
I really don't think its that ambitious a goal, It's the norm for custom containers to provide iterator support to allow use with
Yes, there will be some cases where
Yes, there will be existing APIs which don't map exactly onto that but its easy enough for library authors or end users to provide wrappers around this. Again, over time I'd imagine this will solve itself and become a non issue. using IMemoryOwner<byte> something = MemoryPool<byte>.Shared.Rent(1024);
something.thatThrows(); The memory owners dispose function will be called and the memory returned to the pool. I still think there's good value in providing a feature based on a fixed method of cleaning resources, it makes code easier to reason about, assists with self documentation of types, allows consistent use of structural typing to represent any type managing resources, and over time API oddities will be resolved as they were in the dotnet and jvm ecosystems when they started to provided similar functionality around |
Sorry you feel that way. You may find that there are some developers who think that good naming goes a long way. This is your proposal, and a proclaimed goal is to unify resource handling across the eco system. I'm a bit skeptical about I did suggested relying on metadata, because then you can shift the naming problem to the user and still have well established semantics (which is what this is really about, no?), but you can't both say that the name doesn't really matter and that there should be one name to rule them all. And again, if you hope to standardize things posthoc, it's not something that is so easy to pull off. I mean, you're trying to address a pretty common problem, but an integral part of your solution appears to me to be that you also want everyone to solve it in the same way. Then that one way must be easy to follow and yet flexible enough to work most of the time. Also, the design seems to skip over the fact that |
sorry, you are right. i got confused with "class" examples and missed the point regarding autoclose keyword. i am still think "defer" have better fit for a problem since it is more "self-describing". User have a method for a task right here where it needed while autoclose hides the real "closing code" somewhere else. Not a problem if user the author of the codebase, but for reading code of other users "defer" is more straightforward. But i got your points, thanks for clarification |
Of course naming which reflects whats happening is something to be strived for and I obviously wasn't saying otherwise (dry I also don't think the eco system as it is right now is particually fragmented, so I don't think its quite the ordeal you seem to be implying it will be. I did a quick look through some of the most downloaded and well known haxelibs to see what they provide in cases where the user is able to clean resources.
I gave these repos and clone and searched for close, dispose, clean, release, and other similar terminology. I ignored classes which were externs and ignored cases where sockets, i/o streams, or other classes from the standard library were being directly subclassed, so the authour had the choice to not use the While a very small and basic sample and there are definitely outliers I don't think it paints an initial picture of a highly fragmented eco system of everyone doing things wildly differently. If the standard library uses a consistent naming for cleaning resources (which it mostly does) then users are likely to follow those conventions when building on top of it, when in Rome and all that, and that for the most part seems to have naturally occured. Even the cases where close was not the used terminology it was still all seemingly bog standard Void->Void functions. It's very possible there are big libraries I've missed which go completely off piste with their resource management and make extensive use of different mechanisms, and it would be good to know about them so there are concrete examples to look at. I debated about putting notes on exceptions from close but didn't in the end didn't as I don't think there's anything special to do. I'll update the proposal with some of these details. So looking at that attemptToClose example, no, its not usable and thats by design. With the above advice in mind if the authour wanted it to be usable from autoclose / with hopefully they'd recognise they've overloaded close. Close for a file stream should be for closing file descriptors and other such cleanup which shouldn't fail. Any extra functionality around logging or retry attempts should be put in a separate function where failure is acceptable. |
Actually closing a file descriptor can return an error (though on Linux it'll still end up closed), there is a section in the man page about this.
That would require keeping track of the closed state, since the underlying system calls are not idempotent. |
Yes the posix It would require modification to the existing implementations and may be difficult / impossible to completely do it given all of the targets, but any sort of consistency improvements in that area would only be a good thing. I'd definitely want to see this sort of stuff clearly defined for asys and have mentioned as such. This is all probably getting a bit off topic now anyway, improvements to close consistency would be good even if none of the three discussed ideas are ultimately adopted and we still have to manually call it. |
Some aditional considerations in favor of defer. Proposed autoclose behavior - var tagging at instance creation - implying creation is also always an initialisation. But in many cases resource initialisation - and corresponding cleanup - is not directly linked to creation process, since real (functional) initialisation can be split/branched/delayed/etc according to functional logic. This is a frequent case when class owns several types of resources, required for different aspect and/or those resource usage is conditional. In some cases resource ownership can be "blurred" between components and corresponding cleanup logic became "blurred" too defer allows to inject cleanup at later point and avoid cleanup when its not needed. defer also allows different cleanups according to real logic, aligned to execution logic. with autoclose the only option for such case is to have many “init flags” and “universal close” method ends up with heavy branched cumbersome system with inverted logic structure (relative to initialisation). In edge cases this became unreadable and error prone - and this things are hard to debug. With defer you just have small dedicated cleanup per "resource logic usage concept". And execution bloat adds only if (and when) it is really needed. no explicit “initialisation logic inversion” needed. defer really helps in such cases. Btw, why not have both?
compiler can silently unwrap (2) as “var xxx; defer xxx.close()”, so its basically the same, imho. in (2) even cleanup method can be taken directly from declaration: "defer finalise var xxx=..." -> "defer xxx.finalise();" with some defaults for "autoclose var ..." |
Yes, not a lot of difference between "autoclose" and "defer close" for the added flexibility. Not sure I like the work 'defer' though - seems 'deferred' would be better. But if the usage already exists, seem ok. |
Here's a suggestion: class Foo {
public function new() {}
@:cleanup public function myCustomDestructor() {}
}
class Bar {
public function new() {}
public function closeWithoutMeta() {}
}
@:cleanup var foo = new Foo();
@:cleanup(closeWithoutMeta) var bar = new Bar();
|
Many classes contain a
close
function responsible for cleaning up handles, native resources, or other things which are outside of the control of the GC or which may not want to be kept around until the GC runs. This proposal introduces a newautoclose
keyword to variable declarations which ensures theclose
function is automatically called when execution of the current block ends.rendered