Hacker News new | ask | show | jobs
by tialaramex 1764 days ago
> but there is still a big productivity gap between the borrow checker and GC.

The borrow checker and GC aren't really doing the same thing though. One very important distinction in some fields (but unimportant in others) is that GC only really cares about memory resources. So you are (or should be) doing explicit manual cleanup for all non-memory resources in a GC language. Rust isn't, in Rust we don't write explicit resource cleanup for a programmable interrupt controller, or a file, or a database connection -- the resource knows how to manage itself, and Rust promises to tell it immediately when it falls out of use, whereas a GC can't promise to ever clean up which is why Go doesn't even bother providing a means to do this automatically for your non-memory resources when they're garbage collected (Java does, but again, no promises it ever fires, so, don't rely on this).

3 comments

> Go doesn't even bother providing a means to do this automatically for your non-memory resources when they're garbage collected (Java does, but again, no promises it ever fires, so, don't rely on this).

It sounds like you are talking about finalizers? If so, they exist in Go in a similar way to the Java feature [1].

[1] https://pkg.go.dev/runtime#SetFinalizer

Finalizers aren’t quite the same. They run when memory pressure exceeds some threshold, but the GC analog for borrow-checking is something that will clean up a resource when the pressure on the resource in question exceeds some threshold (e.g., file handles). And even then I’m not sure if that ticks all requirements for resource management.
I meant to respond only to the quoted section comparing Java and Go, which states that Go doesn't have a way to do cleanup on GC (finalizers). I agree with you (and the two other replies and the quoted section itself) that finalizers are not a replacement for RAII etc.

I don't use them for resource cleanup, but I think they probably make sense for library authors as a back-up mechanism (for example, the standard library has a finalizer to close file handles, and the current runtime does GC every two minutes in the absence of memory pressure). You can also explicitly run the GC in Go, and while generally there's a lot of problems with that idea I could maybe imagine it being viable for certain unusual workloads.

You usually want to release non-memory resources deterministically, but finalizers in C#/Java/Go are not deterministic. That's why finalizers usually are not used for resource management. Instead, C# has using statements, Java try-with-resources, Go defer statements.
Finalizers are not guaranteed to run. They're not ersatz destructors.
Yes, if you look at the quote I was responding to, it appears they are saying Go doesn't have finalizers, which is incorrect.
Huh, I also thought Go had no concept of finalizers. That is a bit... awkward way to set a finalizer. But thinking it again, it may just be due to my unfamiliarity.
That is a misconception, RAII like code can be achieved in two ways:

- defer (possibly add a go vet check for when missing it)

- when Go 1.18 brings generics, the withFunc pattern from FP where lambdas get given a resource released on function exist, thus having regions/arena like resource management

The description for go vet says, "it should be used as guidance only". Go vet is in fact a linter (go lint is also a linter, but one focused on style).

So what you're doing there is adding a control to try to mitigate the consequences of a language failing. Go doesn't actually prevent you from getting this wrong, but a linter such as go vet can flag cases where it suspects you screwed up, and maybe you'll catch the worst mistakes most of the time this way and by having usage conventions.

We shouldn't mistake this for equivalent capability. The complicated cases that tempt people to ignore or switch off such linting are exactly the cases most likely to have an undetected problem.

So this is "RAII like" only in the sense that "Just remember to do it properly" is RAII like, and we could make exactly the same claim for C.

Just like you need to use clang tidy to fix all the issues with writing proper C++ code.
Fair. However, there continues to be an unresolved tension in the C++ community about whether they want C++ to focus on its considerable legacy by ensuring enduring compatibility ("No ABI breaks, ever!") or to continue growing and changing even if that means not everybody can follow ("Performance trumps compatibility, ship it!"). Although we can anticipate some sort of compromise, it just isn't possible to have a C++ 2x that delivers everything the modernizers want yet still runs people's technically conforming C++ 11 code unchanged with that binary DLL they've got no source code for. Some people will be unhappy, maybe everybody in the C++ community will end up unhappy.

In the "no breaking changes" case I agree that C++ objects that live in the free store are in the same place as Go, the programmer has a responsible which they may not fulfil, to manage this resource and a linter can only help mitigate this problem.

But plenty of people including Stroustrup want to do lifetime management, despite potential breaking changes from that, and under lifetime management the compiler has visibility into your object lifetimes and can reject programs which inadvertently leak.

Now, that doesn't (can't) make leaks impossible, but it means any leak is now in some sense "on purpose" and would happen for GC'd resources too, it isn't just an accident. For example Rust's mem::forget will prevent the drop happening for the object you're forgetting, but it's not as though you type mem::forget() by mistake. You clearly wanted to achieve that (e.g. you stole the underlying Unix file descriptor from a File and sent it over a socket to a separate process, so now cleaning up that descriptor is the Wrong Thing™) and incorrect usage is not the same category of error as forgetting a with clause in Python.

defer (/using/with) all require the programmer to remember, and write, O(use site) times, the code to release the resource.

RAII destructors, on the other hand, do not permit the coder (at the use site of the type) to forget, as the destructor is invoked automatically when the variable goes out of scope.

In my time reviewing Python (also GC, "with" provides a similar functionality), this is a very common error.

Apparently you missed my go vet reference on purpose, just make all of us aware of it not being the same.

Same applies to using on .NET, where forgetting to call using on an IDisposable type can trigger a compiler error, via Roslyn plugins.

Also seem to be unaware how closure based RAII works on FP languages.

RAII isn't triggered on heap allocated objects unless smart pointers are used everywhere.

Python doesn't have a static analysis tool for with, nor does it support value types with stack allocation. Not all GC languages were born equal.

> RAII isn't triggered on heap allocated objects unless smart pointers are used everywhere.

It's triggered unless you have memory leaks.

So it isn't.
Correct, but except in very special cases, memory leaks are themselves considered bugs. Using memory leaks as an example of RAII not always working is, in my mind, similar to saying Rust doesn't prevent memory errors since it has the "unsafe" keyword.
Agreed, but per my original post, the additional safety that Rust affords protects against a very small share of bugs in SaaS applications. The value proposition isn’t good because you trade so much productivity for a small improvement in quality, and productivity is at a premium and quality (sadly) can be mitigated in other, less costly ways (e.g., progressively rolling out new changes, continuous deployment i.e., bugs can be patched in hours rather than months, etc).