Thoughts on working with numbers in Zig

Thoughts on working with numbers in Zig

Disclaimer: NOT a native English speaker, NOT an expert programmer, NOT an
expert on Zig
.

First of all I do not want this to be some kind of rant, but just to provide
some constructive criticism of what I consider an otherwise near 10/10
programming and development experience.

Now that I have a few months of hobby-programming in Zig under my belt,
including a little stupid hobby game (far from anything “real”), I feel like
sharing my one and probably only real complaint about the Zig programming
language:

Working with numbers in Zig.

The language refuses to guess the intent of the programmer and automatically
cast numbers, lest there is no possibility of information-loss. This sounded
good to me in theory, but in practice, it has turned into a seemingly endless
source of frustration. It is not like other language safety features like a
strong type system or a “borrow-checker”, that you eventually learn and then
they stop nagging you as much. Numbers is Zig will continue to nag you forever!

Below is an example of code, that Zig will refuse to compile:

    for (0..10) |i| {
        const position = m.vec3(i * 1.5, i * 1.5, i * 1.5);
        try backend.drawCube(position, .{}, rotation, "material-name");
    }

Why? Because i is an unsigned 64 bit number, and you are not allowed to
multiply that with a floating point number. Well… I am sorry, but I did not
ask for an unsigned 64 bit number, I just asked you to count from 0 to 9, and
draw cubes with an offset.

This kind of compiler-behavior just feels condescending to me. Like the
programmer cannot be trusted with not causing some kind of bug whenever numbers
are involved. Zig is not like that with other things like memory management.

There are many other examples, I just picked one. Another irritating instance
of working with numbers in Zig, is when you find yourself casting the same
number back and forth just to please the compiler.

And while it probably saves us from making a mistake once in a while, I believe
it to be a net-negative to the development process. Here are some reasons:

  1. It makes programming less fun or more tedious than it has to be.
  2. Messes with the natural readability of mathematical expressions, like these
    two examples:

Natural:

a = b / c;

Working with numbers in Zig:

a: f32 = @as(f32, @floatFromInt(b)) / c;

Which I believe to make it harder to debug the parts of your code which contain
lots of math, like a graphics shader (which, luckily, we write in other
languages).

As an aside, I have noticed that writing parsing (eg. during Advent of Code) and
statemachines (as is often useful in things like games), is particularly
pleasant in Zig (especially the labeled switch). And I cannot help but wonder,
if the reason for that, is that the language designers write a lot of that kind
of code
. In that case, maybe I can hope that the Zig core team will include
someone who deals a lot with math.

Thank you for reading my (not a rant-) post.

9 Likes

The zig team is all for improving the experience with maths, but they don’t want to sacrifice clarity in the process.

It is a balance they are actively working on, at least the theory if not implementing it.

This should address a large part of the issues

for the float side

this thread had some ideas that andrew liked, aswell as some of his ideas.

5 Likes

Not sure I 100% understand, but would these changes increase or decrease “fighting” the compiler/type system? What if all APIs are going to use their own custom number type? Would you not have the problem exaggerated?

Btw, I hope we won’t get the names “real32, float32”. I’m sure we all can remember what r and f stand for in r32, f32.

Nope, an api will either have a technical maximum that they will be able to more accurately encode into types, currently they need an error if their maximum doesn’t happen to be log2.
Or the more likely case is they will be generic over the type.
Also, ranged integers will be able to coerce to other ranges if the other range fully encompasses the original range, e.g. 0 to 10 can coerce to -50 to 50.

This is such a small issue, but I would prefer the more verbose names to highlight the distinction, also for the visually impaired; r and f are quite similar.

1 Like

Alright I hope you are right :).

Me too :D!

In all seriousness, I can’t see any reason an API will take a range that does not reflect the min/max they support. Such things the caller needs to deal with already.

If an API did use a dishonest range, then it’s just bad code, entirely the fault of who wrote the API.

Feel free to prove me wrong :D.

Huh, there’s something I haven’t realized before!

My knee jerk reaction was to say something like

Well, you can’t represent every u64 losslessly as f64, so it would be odd if u64 was implicitly casted to f64. You could imagine iteration syntax like for (0..10.0) |f| which would iterate floats, but that’s also a can of worms, because, for sufficiently large float numbers, a + 1.0 == a.

But then I realized that I can think about i * 1.5 not as implicitly converting i to f64, but rather as multiplying a floating point number by an integer. This seems like a good shift of perspective? Converting u64 to f64 is lossy, so it’s good that we don’t do that. But multiplying f64 by u64 can actually be done precisely, because the loos of precision is already encoded in f64.

It is a bit like

var a: u64 = 10;
var b: i64 = 9;
const less: bool = a < b;

Although u64 and i64 represent different subsets of numbers, and you can’t losslessly convert one into another, it still is logically valid to ask which number is larger.

So, thanks, I think I am now in favor of allowing mixed floating/integer arithmetics (while still disallowing implicit coercions) based on fundamentals. Though, I haven’t written any numeric code in Zig, so I don’t have “in practice” opinion here.

8 Likes

I do think that if you’re capturing a for loop with a comptime-known range, the result type should be comptime_int.
I think the idea is that it might feel like a rug-pull if you switch to a runtime-known range and suddenly it doesn’t coerce as well as it used to, but I personally feel like Zig programmers are mature enough to recognise why this happened.

3 Likes

That would essentially be an inferred inline which defeats the purpose of explicitly not using inline to let the optimiser decide.
Though the optimiser could turn it back into a loop if it thinks it’s better.
IDK how well it would do in that regard, re-rolling a loop doesn’t seem like a high priority to put effort into making the optimiser good at it.

This is such a small issue, but I would prefer the more verbose names to highlight the distinction, also for the visually impaired; r and f are quite similar.

I’m not sure I get exactly what you mean here? Perhaps I misunderstood you/am not the visually impaired group you’re referring to (I’m totally blind and I’m sure lots of low-vision people use Zig, maybe it’s a visual thing), but as a screen reader user, f64, f32, f16 all having the same letter makes more sense than r for some of them.

I think you misunderstand, the ‘r’ isn’t arbitrary, there are big differences between a floating point number and a “real” number.

See this link for more details https://github.com/ziglang/zig/issues/23173

I was stating that I would prefer the names from the proposal: real32 and float32 over r32 and f32. Because visually, the longer names are more distinct. On further thought, I also think the longer names have more distinct sounds, though your opinion would be more valid here.

I shouldn’t be talking on behalf of a group I am not in. If there are any visually impaired (not totally blind) on here, then they should share their opinion.

It is just something that comes to mind as I heard some talk about how their impairment and programming languages interact.

But even people who aren’t visually impaired, I think, would have a harder time distinguishing r32 and f32 at a glance compared to the longer names.

1 Like

Ooh okay, sorry, what you said makes much more sense now. I agree with you, I’d also prefer real32/real64 as a fully blind screen reader user :slight_smile:

Adding for (0..10) |i: i16| to the language would be really useful.

4 Likes

I would definitely take advantage of that, and can’t see disadvantages; could somebody highlight any disadvantages (perhaps other than the “it’s just unnecessary sugar” types, which are easily anticipated)?

2 Likes

Thank you! That is exactly what I was thinking.

A slightly more constructive post: Here is the zig issue (not sure if it will survive?) where Andrew posts that this will not get fixed.

1 Like

This might reopen once the ranged integers make their appearance though, we could loop through a ranged integer.

4 Likes