Roc is a pure language so you'll typically get the same output for a given input. But does this hold for floating point math where things like rounding behavior can vary between different CPU architectures? Also can the compiler potentially reorder floating point math operations to optimize them but in turn make the result nondeterministic between builds?
Hi Martin, I would say that the term "pure" means something different from what this question suggests. A pure function will give you the same output for the same input, on the same machine. It doesn't mean you'll get the same output on different machines. Maybe there's some other term for that, I dunno. But that's not what people mean when they say pure.
Maybe a better way to think of it is that there are no "hidden" inputs or outputs that are not represented in the function signature. It doesn't go off and write to something and read to something unless the function signature tells you that it does.
There was a great blog post on this by Kris Jenkins http://blog.jenkster.com/2015/12/what-is-functional-programming.html
On the second question regarding determinism between builds, that sounds like a misunderstanding. There is nothing non-deterministic about floating point operations. They are completely deterministic. If you run the same "multiply" operation on the same two bit-patterns over and over again, the same bit-pattern will come out the other side, over and over again. That's deterministic.
It might not be the same on every machine but that's not what determinism is about.
So neither of these concepts really apply to the differences between machines.
Sorry, I've probably mixed up my terminology. Here are some more specific questions to avoid the xy problem:
Brian Carroll said:
If you run the same "multiply" operation on the same two bit-patterns over and over again, the same bit-pattern will come out the other side, over and over again. That's deterministic.
Is this true? Won't the multiply depend not only on the two bit-patterns but also on the floating point rounding mode set for the thread that's currently running? And also if the floating point math should ignore denormals or not?
Martin Stewart said:
Sorry, I've probably mixed up my terminology. Here are some more specific questions to avoid the xy problem:
- Can I write a multiplayer game where players only send their inputs to other players and everyone stays in sync?
- Can I write a singleplayer game where I can save the user's inputs + initial state and use it to replay gameplay later on the same machine but on a newer build of the game? (assuming the gameplay code is unchanged)
You will run into the same issues your would run into with most compiled languages (C, C++, Rust, Haskell, ..). In our specific case, we default to using llvm for optimizations, so you will run into the exact same issues llvm based compilers run into. I am not sure what level of floating point optimizations llvm has on by default, but I would not be surprised if it reordered instructions in a way that between two different builds you might get slightly different results.
This isn't really a purity issue, it is really a reproducible builds issue. Though that still may not be the correct term because you want reproducible results on multiple builds and/or computers. That is a much more complex problem that I am not sure any language really manages to guarantee.
Martin Stewart said:
Is this true? Won't the multiply depend not only on the two bit-patterns but also on the floating point rounding mode set for the thread that's currently running? And also if the floating point math should ignore denormals or not?
I would label it as true. Though the rounding mode matters, the programmer should ultimately have control over it. I guess it is just part of the state that you have to consider when running the same "multipy" operation.
Won't the multiply depend not only on the two bit-patterns but also on the floating point rounding mode set for the thread that's currently running? And also if the floating point math should ignore denormals or not?
Wow OK the level of detail is getting very deep here! I thought you were trying to understand functional purity and that you were going in the wrong direction, so I was trying to steer you back.
Definitely some XY problem going on. The game application you describe makes it clearer.
So you want to know if you accumulate floating point numbers over time, will they diverge on different machines. The answer there is it depends on what you're doing with the numbers. As Brendan said, you will have the same issue in any compiled language. This is more about CPUs than languages.
So yeah if you have a system where you're accumulating values over time and expecting them to be consistent across different players across a network you are going to have things like floating point differences, lost network packets, drift between clocks on different machines, etc etc etc and you need to take care of that in how you design your system. Looking for answers in the compiler or language is the wrong place, I'd say.
It's more about how you design your game at the application level. You want to save bandwidth by mostly only transmitting _changes_ rather than the whole state. But every so often you are probably going to have to transmit a bigger chunk of data to reset everybody to some "true" state. You probably can't rely on just transmitting differences and accumulating forever. I think network failure will be a much bigger issue than floating point inconsistencies.
Thanks for the feedback (and sorry about the confusing original question :sweat_smile: ). It seems then like the best approach is to not use any floating point math if I need cross-build cross-platform consistency in part of my program then.
Brendan Hansknecht said:
This isn't really a purity issue, it is really a reproducible builds issue. Though that still may not be the correct term because you want reproducible results on multiple builds and/or computers. That is a much more complex problem that I am not sure any language really manages to guarantee.
I think Java has a strictfp keyword that can be used force floating point math to be consistent (I'm guessing it's done by just using a software implementation of the floating point spec).
Brian Carroll said:
It's more about how you design your game at the application level. You want to save bandwidth by mostly only transmitting _changes_ rather than the whole state. But every so often you are probably going to have to transmit a bigger chunk of data to reset everybody to some "true" state. You probably can't rely on just transmitting differences and accumulating forever. I think network failure will be a much bigger issue than floating point inconsistencies.
Some games (mostly older Real Time Strategies) use a lock-step approach where they do only send inputs to eachother at fixed intervals. It indeed has the issue that if someone's network latency is too high and they miss that internal, then everyone has to pause and wait for them. A more modern approach is lock-step + rollback where if a player hasn't gotten another player's inputs yet, they assume some default input and continue. When they finally get the late inputs, they replay their simulation from that point with the correct input.
Last updated: Jul 06 2025 at 12:14 UTC