4 ms·
In statically typed languages you declare x an unsigned 8-bit integer. Everybody may write to x and still; you'd never anxiously expect x to be anything but an
by mikojan 5y ago
In statically typed languages you declare x an unsigned 8-bit integer. Everybody may write to x and still; you'd never anxiously expect x to be anything but an unsigned 8-bit integer.
There is nothing wrong with "multiple pieces of code" "directly affecting global state" if your business rules are encoded in such a way.
- AnimalMuppet 5y agoThat works because an unsigned 8-bit integer is one piece of data. But the problem we're kind of discussing is a group of objects and/or characters moving in a shared environment. You can't represent that as one 8-bit integer. Instead, it's a bunch of pieces of data: x- and y-locations (and maybe z, as well), extents, x-and y- (and maybe z-) velocities. You can easily get that into an inconsistent state - two objects occupying the same space. This is the point of "not many places write the data": To keep the data in a consistent state, you just have to get a little bit of code working right. To debug it when the data is in an inconsistent state, the problem can only be in a few places. If you're in a multithreading situation, it gets even worse. Yes, it still works for your unsigned 8-bit situation, because it can be written in one assembly instruction. But if your data takes more than one assembly instruction to write, you have to worry about threading. If there are only a few places that write the data, you only have to protect a few places to keep the data from being corrupted by threading issues. (You might also have to protect the readers, so that they can't read it halfway through a series of writes...)
- valenterry 5y ago> Instead, it's a bunch of pieces of data: x- and y-locations (and maybe z, as well), extents, x-and y- (and maybe z-) velocities. You can easily get that into an inconsistent state - two objects occupying the same space. I think that's precisely the OPs point. You model the data (with types) in a way that this invalid state becomes unrepresentable: Map[(x,y) -> object] Now you simply can't have two objects at the same coordinate in your game. Of course you can now come up with more constraints and you will then have to refine your types (and create new ones) to match these constraints. Certain constraints might be hard to express as types in many languages (especially most mainstream languages lack here), but that's not a general problem of the approach but rather a problem of specific languages - for which you then have to find alternatives. > If you're in a multithreading situation, it gets even worse. Yes, it still works for your unsigned 8-bit situation, because it can be written in one assembly instruction. But if your data takes more than one assembly instruction to write, you have to worry about threading. Not with immutable data and functional programming (and I mean that's what the article is all about). This style forces you to make state changes explicit by only making copies and, if copies are made concurrently, explicitly specifying how to merge these copies. Compare: var map = Map(...) fill_randomly1(map) fill_randomly2(map) do_something_cool(map) Now if the fill_randomly functions work in parallel/concurrent you have to somehow ensure that they are called in the right way/order and do_something_cool is called after they have finished. Or worse: if they are supposed to run after each other then you have to be super careful about how your whole code is executed to ensure sequential execution of the parts of your code that calls these functions. Which brings us to your valid point that the places that have to be checked to understand the execution should be as limited as possible. But compare it to the pure functional style where mutation is not possible: immutable var map = Map(...) immutable var map1 = fill_randomly1(map) immutable var map2 = fill_randomly2(map) do_something_cool(???) // What do we put inside? The compiler here will force you to give it a map - but which one? It explicitly makes you aware that you have to somehow merge the results. So let's do that: immutable var map = Map(...) immutable var map1 = fill_randomly1(map) immutable var map2 = fill_randomly2(map) immutable var map3 = merge(map1, map2) do_something_cool(map3) The important thing here is that it does not matter if fill_randomly1 and fill_randomly2 are run one after each other, in parallel or somehow concurrently - it is explicitly specified how the results are merged and the result will always be the same. Also, do_something_cool is guaranteed to run after the other functions, simply because it refers to a variable that relies on the output of the previous functions, so you simply cannot run it "at the wrong time". I believe that was the suggestion of OP here.
- AnimalMuppet 5y agoRight, but then you can't multi-thread merge(). More to the point for the kind of software I write: What if map3 isn't immutable? What if it's something like the state of a router for a TV station, something that keeps changing? Then it can't be immutable, and you need a way to keep other threads from reading it while it's changing (or, worse, writing it while it's changing). Or else, you say that it's immutable, and when some thread changes the state, it produces a new map3, and the old one doesn't change. But then you have the problem of getting all the other threads updated to see the new version of map3. (And, by the way, I've worked on that router for TV stations. Modeling that matrix in a way that invalid states are unrepresentable is, um, extremely non-trivial...)
- valenterry 5y ago> Right, but then you can't multi-thread merge() Not sure If I understand what you mean. It would be both possible to call "merge" in parallel or to implement merge to do the work in parallel as well. > Or else, you say that it's immutable, and when some thread changes the state, it produces a new map3, and the old one doesn't change. But then you have the problem of getting all the other threads updated to see the new version of map3. Yes, but this "problem" is exactly the beauty of this style of programming. It forces you to make your data-flows explicit and hence easy to discover, understand and manipulate/change by other developers. > And, by the way, I've worked on that router for TV stations. Modeling that matrix in a way that invalid states are unrepresentable is, um, extremely non-trivial... I'm not saying it is trivial. But it is possible and depending on the programming language it is actually surprisingly easy. Not only that, with a good typesystem, you not only prevent invalid state from occurring, you even get a lot of support from your IDE due to the extra information it has. Here's an example how that can look like for matrices and functions to manipulate them (like transpose): https://youtu.be/DRq2NgeFcO0?t=1137 https://youtu.be/DRq2NgeFcO0?t=1137 The syntax might be alien to you, but I hope it still shows what's possible at compile-time before even running the program with modern languages today.
- mikojan 5y ago> That works because an unsigned 8-bit integer is one piece of data. No, the same holds true for any struct as well. If you declare x to be {a: uint8, b: uint8} you cannot magically turn it into {x: string, y: string, z: string}. The real problem is that in most languages the expressiveness of this system is severely limited. You cannot do it in Javascript at all, but Typescript gets you very far.