There are often times when coding that some code needs run before a function returns, regardless of how it returns. Examples of this can include unlocking mutexes, or closing database connections. What I propose is support in GDScript for finalizers. There are a couple ways that I have seen this done in other languages:
try/finally blocks. In Java, for instance, you can do something like:try {
// Do a thing
} finally {
// Clean up the thing
}
This works well for langauges like Java that have first class exceptions, but it might be a little heavy handed and verbose for GDScr ipt, since Exceptions aren't really a thing here.
Python-like with statements. GDScript is Python-like, so this might be a feasible approach, but this would make working with 3rd party libraries (especially native libs) a PITA since not all "closeables" could be relied on to implement this, requiring the developer to implement a wrapper class before this could be used. Maybe there is a cleaner way to implement this though. If you have an idea, I am all ears.
Golang's defer keyword. I find Go's defer statement the most elegant approach to this problem, personally. In Go, you could do something like the following:
func foo() (string, error) {
defer func() {
fmt.Println("blaz")
}()
defer bar()
var err error
// something returns an error
if err != nil {
return "", err
}
return "foo", nil
}
func bar() {
fmt.Println("bar")
}
In this example, when foo is called, the first thing it does is "defer two function calls (one anonymous, and one declared). These defererd functions are pushed onto a stack and when the function returns, whether it returns when err != nil or at the end of the function body (_before_ the function actually returns), it pops off each defered function from the stack, and calls it.
This is an elegant approach that is simpler to implement than the others, but there is one issue with this approach in regards to GDScript. Without functions being first class objects, and the ability to create closures, the usefulness of this approach is fairly limited, as having to define defered functions externally means you don't always have necessary variables in scope. When working with pointers, you can get around this by taking a c-like approach and passing the instance in as a function parameter and modifying it from the deferred function, but for value types like int, you don't have this luxury, unless there is a way of creating a reference to the value type and passing that in?
I'm opening this up for discussion here, but I think this would be a huge boon for the language and depending on the approach, could be rather simple to implement.
However its work bit different from go's defer, you already have call_deferred.
func foo():
call_deferred(“bar”)
call_deferred(“baz”, 1, true)
return 0
But I wonder if this common method had some sintax shugar just like get_node has $:
func foo():
defer bar()
defer baz(1, true)
return 0
@jjay They are completely different things. call_deferred queues the call to be invoked later during idle time. IINW, defer executes the code when the surrounding function returns.
The code in a finally block is executed when exiting the scope of the try block, and it's executed always, even if an exception is thrown. I think Python's with is very similar to C#'s using statement which is syntax sugar for a try/finally with the Disposable pattern.
There is also D's scope(exit), which I think is expands to a try/finally.
FYI, you can currently do this by creating a RAII container class, although it's a bit verbose.
For example, here is it for an Image lock:
# Instance one of these
class ImageLock:
var image = null
func _notification(what):
if what == NOTIFICATION_PREDELETE:
# Unlock in destructor, called automatically if instance goes out of scope
image.unlock()
# BTW, we may need an overridable `_predelete` function for this
Another related thing: in GDScript, we don't have unnamed scopes. In C++/C# you can just write braces {...} around a code block which creates a restrained context to work with, without having to create an external function (would loose context too), and allows to make variables in it go out of scope earlier since they may not be needed further in the enclosing function.
But that could be another issue I guess.
A combination of defer and unnamed scopes should be simple enough to not add more to the APIs, basically GDScript just runs whatever comes after defer when exiting the scope, which should not require a capture since it only has to work with what's accessible in that scope (unless I miss something?).
Otherwise it could be like Python with statement, which however requires OOP usage of some sort with new API (having to use an object AND have a function on it dedicated to that statement, which I find quite heavy tbh).
If lambdas were in GDScript we could write only one RAII class that calls any clojure we want. No need for extra keywords.
One more reason to have lambdas. Good.
@hilfazee By lambdas do you mean _actual_ lamdas? Like one-liners? Or do you just mean anonymous functions as first class objects? If the former, that's not enough in my opinion. If the latter, I agree we should have this, but I don't necessarily see it as a requirement, more as an enhancenent. To me, first class functions are somewhat orthogonal to this discussion.
I mean lambdas like in language Godot is written in. Lambdas that can generate clojures:
http://scottmeyers.blogspot.com/2013/05/lambdas-vs-closures.html
Clojure would be passed to an object that is supposed to do cleanup and called in its 'destructor'.
First class functions aren't a requirement because we can add another keyword like "with" or something. But if we had lambdas like in C++ there would be no need for that.
@hilfazer I think we are talking about the same thing, just arguing semantics. :)
I agree, that being able to do something like:
func foo():
defer func():
# do a bunch of stuff here
print("inner!")
return true
while still being able to do something like
func foo():
defer print("defer!")
return true
would be nice. :)
By the way, they're called closures, not clojures. Clojure is a variant of lisp.
@akien-mga what should be the next step here? Everyone seems to be mostly on the same page. I vaguely remember "first class function" support being listed in a previous Patreon survey (though I haven't seen in mentioned since then). Is this already on the roadmap? is there more discussion needed? What can we do to get the ball rolling on this?
Feature and improvement proposals for the Godot Engine are now being discussed and reviewed in a dedicated Godot Improvement Proposals (GIP) (godotengine/godot-proposals) issue tracker. The GIP tracker has a detailed issue template designed so that proposals include all the relevant information to start a productive discussion and help the community assess the validity of the proposal for the engine.
The main (godotengine/godot) tracker is now solely dedicated to bug reports and Pull Requests, enabling contributors to have a better focus on bug fixing work. Therefore, we are now closing all older feature proposals on the main issue tracker.
If you are interested in this feature proposal, please open a new proposal on the GIP tracker following the given issue template (after checking that it doesn't exist already). Be sure to reference this closed issue if it includes any relevant discussion (which you are also encouraged to summarize in the new proposal). Thanks in advance!
Most helpful comment
By the way, they're called closures, not clojures. Clojure is a variant of lisp.