Hacker News new | ask | show | jobs
by noncoml 1387 days ago
I really don’t like the cognitive load of having to remember to use defer. We already have the scope defined, why add something extra?

IMHO the way it’s used in Go is a workaround, of luck of destructors, not a feature.

Edit: not a criticism on your language OP, which is better than what I could have ever built. Just a comment in the “defer” trend.

3 comments

Scoped destruction is awesome in general, and I agree that it is superior to defer.

I think one case where defer might be nicer is for things that are not strictly memory, e.g. inserting some element into a container and removing it after the function finishes (or setting a flag and restoring it).

This can be done with a guard object in RAII languages, but it's a bit unintuitive. Defer makes it very clear what is going on.

> This can be done with a guard object in RAII languages, but it's a bit unintuitive

Some syntactic sugar, like Python’s “with” should help with that, shouldn’t it?

Python context managers are actually very similar to guard objects in C++ and Rust.

What I meant was something like this (could also be done with `contextlib`, but it's also verbose)

    seen_names = {}

    class EnsureUnique:
        def __init__(self, name: str):
            self.name = name
        
        def __enter__(self):
            if self.name in seen_names:
                raise ValueError(f"Duplicate name: {self.name}")
            seen_names.add(self.name)

        def __exit__(self, exc_type, exc_value, traceback):
            seen_names.remove(self.name)


    def bar():
        with EnsureUnique("foo"):
            do_something()
            ...
With defer this could be simplified to

    static seen_names: HashSet<&[u8]> = HashSet::new();

    fn bar() {
        if !seen_names.insert("foo") {
            panic!("Duplicate name: foo")
        }
        defer seen_names.remove("foo");

        do_something();
    }
Honestly, the with example seems simpler if you ignore what it takes to build a context manager (which isn’t all that hard).

Maybe it’s just I’ve never used defer before but I do use python with whenever I get a chance. Not like that, I don’t really understand what the code is trying to achieve by removing the name at the end, but to close resources at the end of the block. And even then only if it makes sense for what I’m doing.

Using a context manager like your example is just busywork IMHO, easier to just write the code out linearly like the defer example.

It's not that it's hard, it's just that it is not inline, so it requires a context switch because the CM is defined outside, even when it's doing something specific.

The most common problem that defer is trying to solve is cleanup when the function returns early (ususally because of an error). Writing the cleanup code inline before the early return results in code duplication.

C#/Java/Javascript have try/finally for this, C has the "goto cleanup" idiom, and C++ and Rust have the guard objects. Go and Alumina have defer.

There's `contextlib.closing` for objects that do not support the context manager protocol and they should be closed.

And then one can simulate defer in the spirit of the `atexit` module with a single context manager (say `finalizer`), defined only once, which could be used as:

    with finalizer() as defer:
        ...
        req = requests.get('https://w3.org/')
        defer(req.close)  # just like contextlib.closing
        ...
        a_dict['key'] = value
        defer(a_dict.__delitem__, 'key')
        ...
        defer(print, "all defers ran", file=sys.stderr)
        ...
The `__call__` of finalizer adds callables with their *args and **kwargs to a fifo or a stack, and its `__exit__` will call them in sequence.
Except go's defer is scoped to the function, instead of the innermost enclosing scope.
That's a good point and also one of the things I kinda like about Alumina. You can do thing like this and the file will only be closed at the end of the function rather than the end of the if block.

    let stream: &dyn Writable<Self> = if output_filename.is_some() {
        let file = File::create(output_filename.unwrap())?
        defer file.close();

        file
    } else {
        &StdioStream::stdout()
    };
So less granular and can be assymetric. Don’t think that’s a good thing
Me neither, I think it is insane.
Despite Nim having RAII of a sort, one thing I love is “defer”, because it lets me get the same semantic behaviour from bound C functions. Handy!