Hacker News new | ask | show | jobs
by aaaashley 4 days ago
Not GP, but I've written game engines and rendering engines. Vector operations are just common enough that having to write `.mul` every time is a huge pain, especially when you put many of them together for a large formula. Compare:

(physics_data.velocity + omega * change) * frame_delta_time

to

physics_data.velocity.add(omega.mul(change)).mul(frame_delta_time)

We learn to read and think about math a certain way, which is incompatible with Zig. Also, Zig's design philosophy of "reading code over writing code" is incompatible with the kind of small modification-test-cycles required when doing games, and creative programming in general. So Zig is sort of DOA anyway for that kind of thing.

But I've been using Zig for non-game projects and it's been fantastic, so definitely not "Blind leading the blind" for the overall language design, imo.

1 comments

I've been thinking about a way around this, and I'd be interested to see if comptime with a DSL wouldn't be too unwieldy. Something like

  math("(v + Ω * c) * Δt", .{ .v = physics_data.velocity, .@"Ω" = omega, .c = change, .@"Δt" = frame_delta_time})
I know this is already possible with comptime, though I haven't implemented it yet since I haven't needed vector math in what I'm working on currently. Can't decide whether using math names is better or worse than using the full variable names though.
I have a sibling comment -- having thought about this for a very very long time, zig should really implement binary pseudo-operator syntactic sugar. I don't think this violate zig's spirit of 'no hidden function calls' in that I don't think it takes much of a mental lift to "get" that (_ <+> _) means "heyo this is a function call, not a true operator".
At first I was going to say that I disagreed since you couldn't choose what implementation of addition you wanted, but now that I've read your comment where you import the type of addition used, it's growing on me. Would you have operator precedence, or would it be more like Smalltalk's binary operators?
forced use of parens, or else syntax error.
All this just to prevent people from using + - * / and ^. Why?
Andrew talks about it because it introduces hidden control flow where you're expecting simple operators. In Zig anything that deals with control flow is a keyword (including short circuiting and, which is `and` instead of `&&`).

I'd argue though that the real disadvantage to having overloadable arithmetic is that you're limited to one implementation. This is actually my biggest beef with Rust, namely traits/type classes. It locks you into a single implementation when you may want to do something different based on the context. Zig pushes the dispatch decision to the callsite, not a trait subsystem (see how Zig implements hash mays for example). So I'd personally prefer to use a DSL, since it lets me specify what type of dispatch to use.

Overloadable operators are not an instance of hidden control flow. Overloadable operators represent a user-defined function call, and thus can't influence control flow any more than a regular function. And if regular functions can't do anything weird to control flow (e.g. if your language already lacks exceptions (or even weirder things like Ruby-style procs)), then overloadable operators can't either.

> It locks you into a single implementation when you may want to do something different based on the context.

If you want differing behavior in a certain context, and if you don't want to use a different method to make the differing behavior explicit (e.g. the `wrapping_add` methods that Rust provides on numeric types), then you can use a different type for that context, e.g. the `std::num::Wrapping` type that Rust provides.

> Overloadable operators are not an instance of hidden control flow.

In general perhaps not, but in Zig it definitely does. Zig considers calling a function to change control flow, because it's no longer just an operator but something that can cause side effects, includinh mutating in place. Perhaps control flow isn't the right term, maybe non-trivial would be better?

With regard to wrappers, I personally find them ugly since 1. They bring in indirection, and I have a personal vendetta against unnecessary indirection, 2. Wrapping doesn't compose well and is a pain to shephard between representations, 3. It's harder to make a function generic across different representations, and 4. Wrappers often don't re-export everything available to their underlying value.

> Perhaps control flow isn't the right term, maybe non-trivial would be better?

Indeed, there are plenty of valid reasons to be wary of operator overloading, such as the risk that someone might insert a network call into your vector addition. There's some precedence from C++ in calling an operator invocation "trivial" when it hasn't been user-defined, in general I might go further and say that a good overloaded operator is "well-behaved" when it not only has a non-surprising implementation (e.g. no side-effects) but also its function is congruent with the specific chosen operator (so no overloading bitshift for iostreams).

I’m not advocating this, but it is worth observing that it is yet another problem one could attempt to address with dependency injection, similar to io and allocators.
It's appealing to people who want to understand and control everything they're doing. When I'm using pandas or SQLAlchemy, I have no idea what the code is actually doing. Most people don't care about such implementation details, but some people do.
yes! i had this exact idea. i also thought about integrating geometric/clifford algebra using zig's type system so that you could have one mathematical multivector object instead of complex / quaternion types, etc.
That's the other great thing about using comptime, is you can specify which DSL you want to use for which scenario. You're not locked into one implementation.