Defer attributes
This is more of a "let's see if this idea is worth looking at" issue.
The idea can be implemented in several ways, here is one:
{
Foo* f = mem::new(Foo) @deferfree;
...
f = null; // This is fine
} // the allocation is freed here.
The code above implicitly becomes:
{
Foo *__temp;
Foo* f = __temp = mem::new(Foo)
defer free(__temp);
...
f = null;
}
We might instead envision it like a method call – this is more appropriate if we think invocation will be done as a method call:
File! f = file::open @defer(close); // inserts a defer __temp.close()
It could also be more complex, maybe you'd define something like:
def @DeferFree(&self) = { @defer(free(self)) };
def @DeferClose(&self) = { @defer(self.close()) };
This is clearly a very rough idea. This is more intended for resource handling than memory management.
Here would be a way to do @pool in a flat way:
fn void test()
{
mem::temp_push() @defer(pop);
void* t = tmalloc();
}
Which then would replace:
fn void test()
{
@pool()
{
void* t = tmalloc();
};
}
However, this is not the only way to do "flat" @pool. We can also imagine macros that could insert defers, if so then we'd just do:
fn void test()
{
mem::@temp_push_pop();
void* t = tmalloc();
}
So it's not necessarily the simplest way of doing this. Similarly, we can have a file with close:
File! f = file::@open_with_autoclose();
The downside of macros that insert defers is that this hidden control flow can be hard to understand, even compared to lazy parameters, this s why @defer attributes might be preferable.
Allowing macros to insert defer into parent scope feels like opening a big can of worms.
But I think there should be something in this direction. One example I've found when annotating code for profilers, when you want a scope to be profiled, inserting a single line @zone_scoped(); would be much better than having to wrap the entire scope (often the entire function) in braces which makes code that is compiled out 99% of the time very intrusive. Currently zone_begin(); defer zone_end(); is the only one-liner alternative to the trailing body macro.
As we discussed on Discord, the proposal of @defer attribute is along the lines of Python with statement. I wrote maybe 500k+ of lines in Python, so I've tried my best to collect all my experience with the snake language and outline a broader picture, if you will.
Python with
There are several cool features with python's with statement:
- It acts as a context manager: initializes the object and does cleanup when scope is exited.
- The scope itself is a mental model. When we see entering into scope, our brain unconsciously switches its context. Scope also wraps around a chunk of code which has a same purpose, solving an isolated set of tasks.
- It's very cool when working with short-lived objects and resources (files, http connections)
withstatement in Python also has a temp variable feature, e.g.with open("file.txt", "r") as fh <- variable- this is a super cool, because we don't clutter function local variables and we can use shorter names for shorter scopes. It's less typing :)- Python allows combining multiple
withstatements for the same scopewith open("file.txt", "r") as fh, open("file.txt", "r") as fh2:
Pros
In these cases I found with useful:
- Temporary file operations - open, real all, close... open, dump all, close
- Acquiring locks / mutexes
- Sockets/ DB connections / stuff like that
- getting short variables from long names
with my_factory.production.mega_class as pmc: - Using
within unit test mocks
with mock.patch("my_func") as mock:
mock.return_value = False
func_using_my_func()
with mock.patch("my_func") as mock:
mock.return_value = True
func_using_my_func()
- Since Python is a garbage collected language, so the resource management problem is secondary. In C3, I think, the mechanism like
withis a game changer.
Cons
- Everything is good in moderation, when
with()is abused code become less readable. - Size of scope should be relatively small (to my taste, 20–50 lines of code) otherwise it would make an impression of spaghetti code. The same problem as with huge if/else scopes.
Thoughts about C3
We have @pool pattern is used all over the place, I think it's good to extend it to other use cases (list is similar to Python's cases above).
with( f: file::open("file.txt", "r")! )
{
f.write(stuff);
}
// file.c3
module file;
fn File open(String path, String mode) @deferable(File.close) {
}
fn void File.close(File self) {
}
Another example:
mut = Mutex();
with( mut.lock() )
{
f.write(stuff);
}
// mutex c3
module mutex;
fn bool! Mutex.lock(&self) @deferable(Mutex.unlock) {
}
fn void Mutex.unlock(self) {
}
Things I don't like
Philosophically speaking, the function is like an article (it has a common topic), scopes of the functions are paragraphs in the article, lines of code are sentences, and operators/calls are words. Without scoping, the code may read as a wall of text. Because of this, I prefer to stick to one line - one thought principle.
This 1st line of code actually contains 3 thoughts: open the file, rethrow the error, close
File! f = file::open("file.txt", "r")! @defer(close);
f.write(stuff);
C-style version:
File! f = file::open("file.txt", "r");
if (catch err = f) return err?;
f.write(stuff);
f.close()
Explicit version:
File! f = file::open("file.txt", "r");
if (catch err = f) return err?;
defer f.close()
f.write(stuff);
Middle ground
File! f = file::open("file.txt", "r")!;
defer f.close()
f.write(stuff);
Alternative universe:
with( f: file::open("file.txt", "r")! )
{
f.write(stuff);
}
What about long examples?
I mean looooong!
File! f = file::open("file.txt", "r")! @defer(close);
<<
+200 lines of code here
>>
f.write(stuff);
C-style version:
File! f = file::open("file.txt", "r");
if (catch err = f) return err?;
<<
+200 lines of code here
>>
f.write(stuff);
f.close()
Explicit version:
File! f = file::open("file.txt", "r");
if (catch err = f) return err?;
defer f.close()
<<
+200 lines of code here
>>
f.write(stuff);
Middle ground
File! f = file::open("file.txt", "r")!;
defer f.close()
<<
+200 lines of code here
>>
f.write(stuff);
Alternative universe (reads worse, but scope indicates we could be working on something):
with( f: file::open("file.txt", "r")! )
{
<<
+200 lines of code here
>>
f.write(stuff);
}
For longer version, I would prefer C-style goto fail; or free resources at the end of function. I don't have much experience with defer, it has a cool features of keeping init and de-init code in the same place. But also I found myself returning to the middle of the code, asking myself if I did defer the resource allocation. Currently, it's somewhat a mental overhead, but I think I will get used to it eventually.
Long story short, I would stick to "middle ground" approach (#1830 kinda thing), and maybe think about introducing "alternative universe" version. Including refactoring @pool usage to more universal with or @with, and adding compatibility with this mechanism to std lib.
Ah forgot, multiple expressions in with too:
with(
f: file::open("file.txt", "r")!,
another: file::open("file.txt", "r")!,
mut.lock()
) {
f.write(stuff);
}
Maybe one could require a double @@ prefix to make it clear there is some hidden control flow. Or require to append a special attribute when calling. Just in hopes to discourage C++-like RAII/implicit destruction a little.
fn Zone @@zone(String name) @defer(zone_end) {
// ...
}
fn zone_end(Zone zone) {
// ...
}
fn void foo() {
profiler::@@zone("foo");
}
One could also have @body implicitly capture the remainder of the scope the macro is invoked in, and this would also have to be clearly indicated at the calling site.
macro Zone @@zone(String name) {
$if env::PROFILE:
Zone zone = zone_begin(name);
defer zone_end(zone);
$endif
@body();
}
It's not greatly prioritized, but it's one of those quality of life things – not having to open a scope unnecessarily. If we have @defer(close) as an attribute saying "store the result of this call in a variable, then insert a defer to invoke the method mentioned"
This example is actually not the interesting one. This one is:
File? f = file::open("foo.txt", "rb") @defer(close);
process_file(file::open("foo.txt", "rb") @defer(close));
– allowing chaining.
On the other hand, you can't do this too much or it will be unreasonably long:
process_file2(file::open("foo.txt", "rb") @defer(close), file::open("foo.txt", "rb") @defer(close));