Eschewing all hidden function calls is a reasonable design choice, but you give up a lot.
No destructors means no smart pointers – and Zig doesn't seem to have a garbage collector, so I guess you need to free() things manually.
No operator overloading means, among other things, no custom numeric-like types – including wrapper types for units (e.g. Feet) unless the language provides separate support for those (e.g. Go does, Zig seemingly doesn't).
>No operator overloading means, among other things, no custom numeric-like types – including wrapper types for units (e.g. Feet) unless the language provides separate support for those (e.g. Go does, Zig seemingly doesn't).
Prior to their invention, most drivers survived without airbags in their cars too.
comex just noted that the design decision here has some tradeoffs. One of those tradeoffs is that you can't define wrapper types for units. Such a feature has clear use cases - it's not hard to find instances where unit conversion bugs have had severe consequences. "Most languages survived without that" is not a constructive comment.
That's not what parent said. You can express your maths as function calls.
Operator overloading is often misused and violates the principle of minimal surprise. You could even have side effects.
Still, type safety is totally possible. In Go, of your unit is Feet, you wrap your int into Feet and you cannot sum it with anything but feet. And that's all compile time, no cost.
It's not just some historical curiosity from privative computing days.
Huge successful current languages don't have operator overloading now either. If anything, outside numerical/scientific work, it's even frowned upon.
And even in languages that do have them, it's not like people usually work with typed feet and such units. It is indeed safer, but I'm not sure it's mainstream (F#, Ada, Rebol, etc). Of course one can find such typed unit libs for other languages with Op. Overloading (C++ eg. has boost.units), but it's not like they're mainstream even where Operator Overloading is available.
In any case, it's not the first argument to make against a system language that it doesn't support Op. Overloading as a means for this use case.
C++ and Rust need destructors because they both have unwinding and thus need to know how to destroy things. If there is no unwinding then calling destructors manually is not that bad if compiler checks that you do it. That said, I haven't used zig and don't know how it actually works.
I asked this in another thread, but how do generics work in the absence of destructors? Let's separate the idea of a destructor from the idea that a destructor is implicitly called at the end of a scope.
In C++, if there weren't automatic destructor calls, you could still clean things up manually with a pseudo-destructor call, x.~T(), where T is the type of x.
In C, there are no (user-defined) generics, so you always know the concrete type of x, so you know whether it needs to have a destructor called and what the name of that destructor is.
In Zig, how would generic code create an instance of a generic type T that may or may not need to have a destructor called, ie. how do you know if you need to use defer and what the deferred expression should be? How would tooling be able to check that destructors were called if there is no special way of marking that a destructor exists?
Having used Zig a bit, and C++ for years, I personally believe that this sort of scenario is pretty rare. You can always add a no-op dealloc method to your type if you want a generic function to handle it correctly. Moreover, Zig has awesome metaprogramming support so you can probably think of a way to check the type parameter for a dealloc method and only generate the dealloc call for those types.
I'm not sure how to reconcile this statement with the following claim from the linked article: "Zig has no macros and no metaprogramming yet still is powerful enough to express complex programs in a clear, non-repetitive way." Not only does it claim that Zig has no metaprogramming, it suggests that it sees metaprogramming as undesirable.
Pretty rare? What about ArrayList? I want ArrayList(T) to work whether T = i32 or T = ArrayList(i32).
> You can always add a no-op dealloc method to your type if you want a generic function to handle it correctly
You're looking at it from the perspective of the generic consumer, not the generic author. The generic author generally does not have the ability to edit the type. Plus there are many types that do not have methods at all (integers, points, etc.).
> Moreover, Zig has awesome metaprogramming support so you can probably think of a way to check the type parameter for a dealloc method and only generate the dealloc call for those types.
Yes, you can detect and call a method called deinit that has the appropriate signature using @reflect. You can even put this in a function and call it a pseudo-destructor. But there's no guarantee that deinit has the actual semantics of a destructor without some kind of language-level agreement between class authors.
OK I see your point. Worst case scenario, you force the caller to pass a destructor as an additional argument (e.g. ArrayList(T, fn(*T))) C++11 allows for this in the smart pointer classes.
Not sure what you mean. The elements of an ArrayList(ArrayList(i32)) are ArrayList(i32)s.
To answer my question, no, they're not deinited. All deinit does is call self.allocator.free on the slice of elements, and for many allocators that's a nop. In fact none of ArrayLists methods take any kind of ownership of its elements. If you shrink an ArrayList(ArrayList(i32)) by one you leak the last ArrayList(i32). None of the methods call destructors on the elements because there is no generic notion of a destructor, only particular ad-hoc ones like deinit methods. ArrayList appears to solve the problem I mentioned above about generics not knowing if a generic type needs to have a destructor called by only supporting types that don't.
In C++, you'd write the function that clears a vector something like
void clear() {
for (auto p = ptr; p != ptr + len; ++p) {
p->~T();
}
len = 0;
}
For a vector<vector<int>> the syntax p->~T() calls the destructor on a vector<int> element. While for an vector<int> the syntax p->~T() is a pseudo-destructor call, ie. it does nothing. This makes the same generic code work when the elements of a vector need to have a destructor called and when they don't.
> Not sure what you mean. The elements of an ArrayList(ArrayList(i32)) are ArrayList(i32)s.
I believe what he was wasking was "given an ArrayList(i32), how would you expect it to call deinit on the member i32s?". The answer, of course, is that you don't, which is also true of ArrayList(ArrayList(i32)). ArrayList absolutely supports heap-allocated types, you just have to free them yourself before calling deinit() on the ArrayList itself.
Which only brings us again (putting aside what virtue recommends that design over having destructors) to the same original question. If I have an ArrayList(T) for a generic type T, how do I know if the elements need to be freed before I deinit and how do I do that if they do?
I'm having trouble imagining a scenario where you'd have code that was so generic it could take an ArrayList of any arbitrary type and would also be responsible for its destruction.
But if you did, you could use `@TypeInfo` to inspect the inner type for a function named `deinit`, or some other criteria that made sense for this determination.
Any generic data type with a private ArrayList(T) member is an example. Unless you also expect callers to manually run destructors for elements of a type's private members, and their private members, etc. And it's not just about destroying the ArrayList entirely. Any function which just removes elements from an ArrayList needs to know how to destroy elements or else pass the buck for half its purpose to the caller. When I call shrink on an ArrayList(ArrayList(i32)) I'm supposed to preloop over the shrunken-over elements and call deinit on them before I call shrink? When I call the function that removes elements satisfying a predicate I'm supposed to preloop over the elements and call deinit on the ones I'm about to remove? Obviously not.
I already mentioned that hack. All you're doing there is introducing the ad-hoc notion of a destructors as a method named deinit. Again, in order for that to work in generic code that convention needs to be blessed by the language.
That's entirely the point; he/she isn't expecting to call deinit on an i32, but is expecting to call deinit on an ArrayList(i32) if said ArrayList(i32) is in fact an element of an ArrayList(ArrayList(i32)). And calling deinit on an ArrayList(i32) is of course valid, nontrivial and necessary, since such a value needs to own a heap allocation that must be free()d.
C has had generics since C11. I’ve got a clone of “the good parts” of the std C++ template library (vector, unordered_map, ...) laying around somewhere.
Assuming that types 'vecint' and 'vecdbl' have been defined by the user. This does single-parameter parametric type dispatch. There's way to allow the user to use table-driven macros ('X-macros') to allow per-translation-unit definition of an arbitrary number of instantiations.
The nice part, over C++, is that the generic instantiation is localized to a single translation-unit file, so you don't have to yell at the junior devs for instantiating templates without using an 'extern' construct.
No, it's very likely about _Generic. <tgmath.h> was added to C99 in an attempt to steal Fortran mindshare, ie making numerics and scientific computing less cumbersome (the same is true for other features, eg complex numbers or restrict). But in C99, it was still special-cased. The next standard C11 introduced _Generic so people could create their own type-generic interfaces.
For local variables, I'd say `defer` is an okay but not great substitute. If you call malloc() yourself, then sure, it's not hard to remember to stick a "defer { free(ptr); }" afterwards. But what if you call a function that returns an owned value? Can you tell just from looking at it that you're meant to free the thing afterward?
CoreFoundation, a C library on Apple platforms, has a cute solution to this: a strict naming convention. [1] If a function returns a value at "+1" reference count, i.e. you have to release it yourself, the function's name should include "Create" or "Copy", even where that would otherwise seem unnatural. For example, "CFSocketCopyAddress" returns a socket's address as a CFDataRef, returned at +1. You might expect it to be called "CFSocketGetAddress", but "Get" is reserved for functions that return values at +0.
As I said, cute – but fundamentally a workaround for language limitations. Distinguishing between two kinds of functions is exactly the sort of menial bookkeeping that computers excel at, whereas humans are prone to making mistakes. And – while CoreFoundation is forever stuck with the goal of C compatibility, Objective-C, which used to rely on a very similar system of manual retain/release and naming conventions, did ultimately transition to automatic reference counting.
But it doesn't have to be automatic. One use for destructors is to automatically release locks, and an interesting alternative design for that exists in Clang's "thread safety" annotations. These are annotations you can add to existing C or C++ code that uses mutexes; their main purpose is to catch you if you access a variable protected by a mutex without owning the mutex, but they can also complain if you lock something and forgot to unlock it. You can annotate functions as "requires mutex" (i.e. you must call it with the mutex locked, and it stays locked after the function), "acquire" (call with it unlocked, returns with it locked), "release" (the opposite), and so on. I think I once tried to abuse the annotation system to enforce reference counting, but it was broken. But I'd love to see a more thorough and robust design along these lines.
No destructors means no smart pointers – and Zig doesn't seem to have a garbage collector, so I guess you need to free() things manually.
No operator overloading means, among other things, no custom numeric-like types – including wrapper types for units (e.g. Feet) unless the language provides separate support for those (e.g. Go does, Zig seemingly doesn't).