Friction in programming language design

TIL. Thank you. So void => anyopaque, *void = *anyopaque. cool ty ty.

1 Like

The issue is the type of lpReserved: *void, which expects a non-null value.

The correct declaration should be:

pub extern "kernel32" fn FooBar(
    in_hFoo:      std.os.windows.HANDLE,
    in_lpBuffer:  [*]const u8
    lpReserved:   ?*anyopaque
)

Which should compile the following code without issues:

const rv = win32.FooBar(h, str, null);

Because ?*anyopaque allows null values, this should compile.

The point is: no value can be null if it isn’t optional.
The compiler will always complain if you try and do so.

If null is a possible value, make the type optional with a question mark: ?*anyopaque.

3 Likes

No, void in C is void in Zig. They are both “unit types”, i.e. types that only have a single value, and are used as the value “returned” by control flow instructions that are only executed for side effects.

void* in C, however, is indeed *anyopaque in Zig – a pointer that points to something of an otherwise unspecified and/or unknown type.

Is it a mess? Yes; blame C for that. Not only is it overloading the name void in the context of types, even the less confusing usage of it that I mentioned first is still wrong. It’s just a mess all around.

Why is 'void' in C wrong?

Because that “void type” actually means “a type that doesn’t have any values at all”. An empty enum is an example of a void type, but the void in C/C++/Zig/etc. is not.

This incorrect usage of “void” unfortunately so widespread, in all the languages that descend from C, that Zig creators made the pragmatic decision to just go with it, too. Zig does have a true void type, though; it’s just called NoReturn and is only used to denote the (lack of) return value of diverging functions.

3 Likes

I don’t entirely disagree (at least, I haven’t found it to be a problem in any code I’ve written yet), but I do think making try syntax sugar for returning the error instead of making it syntax sugar for narrowing the error is a mistake if you’re trying to encourage robust error handling.

//currently equivalent
fooCanFail() catch |err| return err;
// =>
try fooCanFail();

vs

//better?
fooCanFail() catch |err| switch(err) {
    .barError => handleBar(), 
    else => |remaining| return remaining;
}
// => 
try fooCanFail() |err| {
    .barError => handleBar(),
}

Then with that paradigm the current try would be

try fooCanFail() |_| {}

Which isn’t much longer, but stands out. I think that would be a good thing.

1 Like

Is it a mess? Yes; blame C for that

Ah…not just C. :slight_smile:

Occurrences of voids at higher levels of the system should be viewed with suspicion because they are likely indicators of design errors.
— Bjarne Stroustrup, The C++ Programming Language

It is, after all, why my user name is as close to const void* as I could get… there is no greater design error! Nothing says problem like a pointer to a constant thing that is stuffies; “hello type safety my old friend, I’ve come to talk with you again…”

AND YET: THIS DESIGN ERROR IS BAKED INTO THE WINDOWS API.

Mandatory NULL yet void* parameters! Like… I don’t even know where to start.

And the Windows API I am referring to … appears … MANDATORY for console outputs; so, not even some random esoteric edge case, but the very BASIC of base cases for zig to even operate on the windows platform: WriteConsole.

BOOL WINAPI WriteConsole(
   ...
  _Reserved_       LPVOID  lpReserved
);

MSDN: lpReserved Reserved; must be NULL .

The tragedy, for as much as I ike to dunk on this design, it is THE mandatory design to contend with; managing the abstraction differences between C++/zig does break my dwindling brain.

So…how can I help? Where is this stuff written down? I feel like there is a lot of knowledge in this thread alone. Should I start writing a book? Or is there a book to add it to?

I do look forward to the day when there is more native zig functionality then not, and void can be locked into the dark boundaries of the interface between platform and application: libraries.

There are many questionable things baked into the Windows API, and the reason is almost always the same: backwards compatibility. Remember, this is an API where the stated compatibility guarantee is measured in decades, and the actual/unstated one is even longer. It takes a break in architecture (16bit → 32bit → 64bit) for Windows API to change in notably incompatible ways, and in even then the breakage is often just on the ABI level and merely requires a recompilation.

Of course, it’s not like engineers who design such a long-lasting API don’t learn their lessons. The WriteConsole function you mention is clearly an example of such lesson: the final parameter is evidently there so that it can potentially be transformed into a bit field of dwFlags. In other words, the LPVOID you complain about so much comes from the needs of forward compatibility.

Zig documentation on C interop is probably a good start: Documentation - The Zig Programming Language

Those would have to be written in Zig, though, so you’re just kicking the can down the road. Because if they weren’t, you’d have to integrate with them across an FFI boundary, meaning you’d be back to square one :slight_smile:

1 Like

I do think making try syntax sugar

So where I am parked: try should be inferred by the compiler, as should ! in the function return. There is no reason for a person to specify: the compiler knows.

I have been parked here for a good minute, as I do not see the value in extra content that has no purpose other than a mechanical requirement of the language.

2021: https://www.reddit.com/r/Zig/comments/rs1dr2/implicit_try_anyerror/

I have been thinking of zig’s design goals and how they could be met, and do believe there are elegant solutions. Time and priority are the ultimate boundaries, but philosophically, maybe an approach to a more implicit / inferred paradigm could expand on the excellent inferences zig makes today? I’ll try to document more thoroughly and cleanly in a diff post.

The WriteConsole function you mention is clearly an example of such lesson: the final parameter is evidently there so that it can potentially be transformed into a bit field of dwFlags . In other words, the LPVOID you complain about so much comes from the needs of forward compatibility .

LOL–yes, Microsoft has legendary backward compatibility, but then also breaks their own API routinely.

To me, general Microsoft API compatibility over time is a non-deterministic function for Microsoft – given an input of future span of time t, flip some coins…maybe it will break, maybe it won’t, maybe it reverts, maybe it doesn’t.

However, up to time now, as long as the so/DLL versions are present, if what was there is there, then what worked will work. que sera sera.

Apple, on the other hand, makes no bones about it - the API will break, buckle up butter cup.

Oracle is also somewhat consistent, but there, we see more of an intersection of jagged acquisition bumping up against mature product.

And SAP…toilet flushing sounds

Those would have to be written in Zig, though, so you’re just kicking the can down the road.

I guess I am not tracking…what do you mean?

If not C or C++, there is typically an FFI somewhere, BUT, that doesn’t mean the application layers of a system should have to deal. That’s why it is crucial for compilers to be cross-architecture (intel + arm) and libraries to be cross-platform (MacOS, Linux, Windows).

The attraction of zig, to me, is write once, run anywhere, built from right here, without the cumbersome object overhead of Java, or the typeless comfort of Python/JS, etc etc etc…

I used to think like that too, but I’ve grown to understand that try is a necessary evil, sometimes it just doesn’t make sense to handle errors locally, sometimes it does, I think this is one of those cases were it would be hard to come up with something better. With that said it does require some willingness, there is a proposal to be able to switch on comptime known array of enums/error sets, and I think if this gets accepted, this would further discourage the use of try. Because sometimes one of the way I use the try syntax, is to isolate into a function one kind of errors to “group them” if that make sense. but if I could just declare static subset of errors and name them to switch on them that would completely remove this use case for me.

This would essentially allow any function to silently ‘throw’ errors. With the status quo, I the programmer have to accept that I am refusing to handle errors at this level, rather than have it happen by accident. Could the compiler do it? Yes. Should the compiler do it? No, i think error handling is something firmly in the responsibility of the programmer, and the choice to defer it should be a concious one.

5 Likes

sometimes it just doesn’t make sense to handle errors locally, sometimes it does

I’m not advocating for the removal of try or enforcement of local error handling. I’m advocating for “propagate the subset of errors not yet handled” being the default for try with “propagate all errors” as the special case when you discard the error capture, instead of how it works now.

4 Likes

If I understand you correctly, this would violate one of Zig’s core principles: “No hidden control flow.”
The try keyword tells the reader of the code that the function can return there like an if ... return. If there is no try or return, the reader can safely assume that statements below that call are always reached.

2 Likes

This would essentially allow any function to silently ‘throw’ errors

How?

The compiler knows if a function throws an error. How is it silent?

A key key capability that zig has - ZLS.

I totally understand why we, as people, may want to see the try, the !, etc. The challenge is…do we really need a person to instruct the compiler what it already knows? Isn’t a human repetition of compiler knowledge a greater opportunity for error?

An IDE could very easily render all the inference needed … it works great w/inferred types, parameter names, etc. Let’s spare the person and make the machine do all the work!

ZLS → IDE could easily render, in a suitable light color, the inferred try; the inferred !; the inferred error set. With this configuration, there is no functional difference. Could zig fmt “auto fmt” on save? Sure…could it be a build option? Sure…I understand there are cases where you want more explicit, less inferred.

But…there are a great deal of cases where inferred is preferable.

Does this change any code already written? No…I am not sure how hard it would be to do. I really really like ZLS and an IDE that renders all the inferences so far, completely the cat’s meow…

If I understand you correctly, this would violate one of Zig’s core principles “No hidden control flow.”

how is it hidden?

clarity isn’t just maintained, it is perfection. Just because a person doesn’t type it, doesn’t mean an IDE can’t render what the compiler knows.

the fact that you need another piece of software to provide you with this very important information makes it hidden, the point of “no hidden control flow” is you know exactly what the code is doing using nothing but the source code. I understand that you are fine with tooling doing this work, many people are not, for a variety of reasons.

why is it ok to make zig harder for them just because its easier for you?

3 Likes

Being easy to read is a stated goal of Zig. It is far better if code is clear to read without the need for external tools, and ZLS is not an official part of the Zig project. I also still don’t see what would actually be gained if try was implicit.

2 Likes

Writing functions that can’t error is also a feature of the language, knowing that a specific function will never return an error is very useful, to more quickly understand code by being able to distinguish those from other functions.

With what you are proposing, there could be 5 levels of indirect function calls before some function implicitly turns the function into something that returns errors. You would have to query the compiler please explain to me how this function works and whether it can error, because the person reading the code couldn’t find out in a reasonable time. Especially if that code also uses comptime / generic code.

Also there are cases where Zig can’t infer the error value and instead requires the programmer to specify the error set.

Function pointers would be really weird too, if they don’t have explicit types.

2 Likes

An IDE could very easily render all the inference needed

Also I’d like to mention that a relevant part of code reading happens outside the IDE.
For example if I review a PR then I do it in github’s web interface, and I do not want to be slowed down by downloading the code and opening it locally in an IDE.

4 Likes

why is it ok to make zig harder for them just because its easier for you?

Why is it either or? There is a great build system…why not both? Flexibility and options…no changes to grammar.

Not saying there aren’t use cases for explicit. Just that there are legit use cases for inferred, especially at the application layer of system design, where the application is integrating this underlying scaffolding of explicit use case.

Consider

pub fn doTheThing() !void {
  try emit(fg[fade_seq[fade_idx]]);
  try emit(txt[txt_idx * 2]);
  try emit(line_clear_to_eol);
  try emit(nl);
  try emit(txt[txt_idx * 2 + 1]);
  try emit(line_clear_to_eol);
  try emit(nl);
}

vs

pub fn doTheThing() void {
  emit(fg[fade_seq[fade_idx]]);
  emit(txt[txt_idx * 2]);
  emit(line_clear_to_eol);
  emit(nl);
  emit(txt[txt_idx * 2 + 1]);
  emit(line_clear_to_eol);
  emit(nl);
}

Fewer words are simpler, no? Esp if the build system says, infer the ! and try. And yet, when you need explicit, 100% no loss in functionality?

Being easy to read is a stated goal of Zig.

How is an option to infer ! and try less legible? Keep in mind we infer variable types, parameter names, etc. You don’t have to … but it is nice that I can.

1 Like