Fellow C and Rust programmers, how do you live without classes?
I lived in a perfect OOP bubble for my entire life. Everything was peaceful and it worked perfectly. When I wanted to move that player, I do player.move(10.0, 0.0); When I want to collect a coin, I go GameMan -> collect_coin(); And when I really need a global method, so be it. I love my C++, I love my python and yes, I also love my GDScript (Godot Game Engine). They all work with classes and objects and it all works perfectly for me.
But oh no! I wanted to learn Rust recently and I really liked how values are non-mutable by defualt and such, but it doesn't have classes!? What's going on? How do you even move a player? Do you just HAVE to have a global method for everything? like
move_player();
rotate_player();
player_collect_coin();
But no! Even worse! How do you even know which player is meant? Do you just HAVE to pass the player (which is a struct probably) like this?
move(player);
rotate(player);
collect_coin(player, coin);
I do not want to live in a world where everything has to be global! I want my data to be organized and to be able to call my methods WHERE I need them, not where they just lie there, waiting to be used in the global scope.
So please, dear C, Rust and... other non OOP language users! Tell me, what makes you stay with these languages? And what is that coding style even called? Is that the "pure functional style" I heard about some time?
Also what text editor do you use (non judgemental)? Vim user here
If you want your code to be performant you need to think about how you lay out your data for your CPU to manipulate it. This case might work well for one player but what if you have 100, 10 000?
When you call player->move (assuming polymorphism), you're doing three indirections: get the player data at the address of player, get the virtual function table of that player, get the address of the move function.
Each indirection is going to be a cache miss. A cache miss means your cpu is going to be waiting for the memory controller to provide the data. While the cpu can hide some of this latency with pipelining and speculative execution, there are two problems: the memory layout limits how much it can do and the memory fetch is still orders of magnitude slower than cpu instructions.
If you think that's bad, it gets worse. You now have the address of the function and can now move your player. Your cpu does a few floating point operations on 3d or 4d vectors using SIMD instructions. Great! But did you know that those SIMD registers can be 512 bits wide? For a 4d vector, that's 25% occupancy, meaning you could be running 4x as fast.
In games, especially for movement, you should be ditching object oriented design (arrays of structs) and use data oriented design (struct of arrays).
Don't do
struct Player { float x, float y, float rotation, vec3 color, Sprite* head};
Player players[NUM];
You will have to write your code differently and rethink your abstractions but your CPU will thank you for it: Less indirections, operations will happen on data on the same cache lines, operations will be vectorizable by your compiler and even instruction cache will be optimized.
Edit 1: formatting
Edit 2: just saw you're doing 2d instead of 3d. This means your occupancy is 12.5%. That operation could be 8 times as fast! Even faster without indirection and by optimizing cache data locality.
Compilers are already really smart and do a lot of heavy lifting but they're also restricted to what you write and they err on the side of safety. They will do things like inline object functions if you don't have virtual functions and are simple enough which reduces the number of indirections. They won't re-order your classes and re-write your code. In my experience compilers don't do a good job at magically vectoring code (using SIMD registers to their fullest extent), so maybe that can be improved by a super smart compiler.
I would say it's possible to have a linter let you know if you're making structs which are cache unfriendly.
There are also runtime tools like Intel's Vtune or perf on Linux. I would say that while those tools are very powerful the learning curve is very difficult. In my experience you need to know a lot about optimization to understand the results.
Today's generative AI can give you broad strokes about refactoring some code to DOD and I'm sure in a few years it could do something to whole projects.
Oftentimes safety comes at the cost of performance with compilers if you don't give it enough details such as restrict/noalias, packing, alignment, noexcept, assume/unreachable, memory barriers. Rust is able to be performant and safe because it is a very verbose and restrictive language when you write it. C++ gives you all the tools but they tend to be off by default. In my experience game devs like to stick to C++ despite the lack of safety guardrails because it's faster to write efficient code and "we're not making medical equipment" sentiments.
I think at that point you could just look into Entity Component System design. I'm particularly fond of Flecs. Here, entities are empty objects to which you can add any number of components. Typically components are void of logic. Instead you write systems that match entities that have the components they need, and then just operate on that data.
I thought about this a few times while programming but general advice pointed me towards OOP so I never actually got to implement anything in Data Oriented Programming the way you described.
Yes ECS is probably the most popular scalable DOD programming pattern, aside from Compute Shaders. If correctly used, ECS store your data in a way that makes access more cache friendly. There are multiple flavours of ECS with some which are better for small components and access and others which are tuned for insert and delete.
One thing I would say if you want to switch to ECS is to start with a simple performance test of say 100, 10 000 and 1 million entities being updated in a loop. Do this with and without ECS. This way you can keep track of performance and have actual numbers instead of trusting the magic of ECS. ECS can have some overhead and aren't always the best choice and if you use them wrong they won't be as good.
I haven't tried Bevy yet but it looks very promising!
Consider struct as the data layout / organization, and impl (of that struct) as the functions & implementation of functionality for structs (and traits). It's basically like separating member variables & member functions.
Late response and you might have already gotten an answer, but what you wrote is exactly the same as:
// Define our player struct
struct Player {
x: f32,
y: f32,
rotation: f32
}
// Define the methods available to the player struct
impl Player {
pub fn move(&mut self, x: f32, y: f32) {
self.x += x;
self.y += y;
}
pub fn rotate(&mut self, by: f32) {
self.rotation += by;
}
}
fn main() {
let mut player = Player { x: 0.0, y: 0.0, rotation: 0.0 };
player.move(10.0, 10.0);
player.rotation(180.0);
}
The code example you wrote does not use anything that is exclusive to OOP languages as you are simply encapsulating values in a class (struct in the Rust case).
Unlike C++, the biggest difference you will find is that Rust does not have the same kind of inheritance. In Rust you can only inherit from traits (think interfaces in Java/C# or type classes if you have ever used Haskell), whereas in C++ and other OOP languages you can also inherit from other classes. In a lot of cases just using traits will suffice when you need inheritance. :)
So in conclusion, no global functions! You still have the same name spacing and scoping as you would in C++ etc!
Ps. I use VScode because it rocks with Rust, and while Rust is heavily inspired by functional programming languages, it is not a pure functional programming language (nor is C) but that is another can of worms.
Somebody needs to RTFM ;) no seriously, Rust isn't something you can just jump into and guess what you're doing. Start with the official book and make sure you understand all of that.
IME the hardest part of Rust was learning the lingo to interpret compiler messages, and getting a solid grasp on references and borrowing. There is a lot more of course, like any language, but to me that was the steepest learning curve. I haven't used it in a few years tho, after losing all interest in programming.
Your OO languages at their core just abstract patterns like using a *this pointer. OO is possible in any language once you understand how it works. You should just go back to C++ or whatever you’re comfortable with.
Hm.. Never though about it that way. I guess that really is how they work, don't they?... But it's all cool and combined in OOP so it works so nicely and stuff.
When you call player.move() are you mutating the state of player or are you really just logically attaching the move() function to the player object in order to keep your code... Logical?
player could actually be an interface to some object in an external database and move() could be changing a value in that database. Or player could just be a convenient name and place for a collection of "player"-related functions or stranger (yet weirdly common): A workaround for implementing certain programming patterns (Java and C#, I'm looking at you haha).
In Rust, attaching a function or property is something you do to structs or enums. It carries a very specific meaning that's much more precise and IMHO more in line with the original ideals of OOP (what they were trying to accomplish) and I think the way it's implemented (with traits) makes it far more flexible.
You can define a trait that requires a bunch of types/functions and then any implementation (impl) of a struct or enum that includes them can be said to support that trait. This allows you to write type-safe code that will work in zillions more situations and across many different architectures than you could with traditional OOP languages like C++ or Java.
It's the reason why embedded rust is kind of taking the world by storm right now... You really can "write once, run everywhere" thanks to careful forethought from the Rust developers and embedded-hal.
In my case I want to move that player, meaning, changing the position of that player object (probably gonna be a vec3 or vec2). So like this:
void move(vec2 by){
this -> position += by;
}
I will look into impl. They do seem very useful from what I have heard from the other commenters on here. Thank you for contributing and sharing your knowledge!
The only thing that makes rust different from cpp is the lack of inheritance. We have classes, they are called structs. And Interfaces, they are called traits.
But instead of inheritance if you want shared behavior between two structs you need to have both of them implement the same trait. So instead of
fn pet(aimal: Animal)
You'd have
fn pet(animal: impl Petable) // polymorphism via monomorphization
Or
fn pet(animal: &dyn Petable) //polymorphism via dynamic dispatch
Instead of writing an animal super class you define a Petable trait:
trait Petable{
fn pet(){}
}
We even have operator overload, because you can simply implement the f32::Add (or whatever) trait for your structs.
I don’t program in Rust, but IMO non-mutable by default is how it should’ve always been. It’s more reasonable to make values mutable out of necessity - not make them constants just because you can. Even in OOP I think you should avoid using variables when possible, as they commonly give rise to logical errors.
I think it’s harder to reason around programs that heavily use variables. It’s easy to tangle yourself into a mess of spaghetti code. You need to read back and forth to understand all the possible states the program can be in and ensure none of these states will break it. “Oh, you can’t call this method on line 50 because some other method call on line 40 changed some internal value, which isn’t corrected until line 60”.
Same code without variables is usually easier to read. There’s only one state to consider. You just read the code from top to bottom and that’s it. Once a value is set, then that’s final. No surprise states.
Variables also tend to make multithreading more difficult to reason about.
Your example with player movement is one example where variables are needed. You should keep using mutables here.
I think all programmers should learn to program in a more functional style. Even if you end up using OOP you can still make use of functional programming practices, like avoiding variables.
Ever since I learned Clojure, I’ve ridden the functional programming train. Now I write Elixir for my day job and even though I still have a soft spot for Java, the first language I wrote professionally, I think OOP in general is a flawed paradigm that makes bad software. But I won’t rant about it, I know these things can be a matter of taste for a lot of people.
In a functional language like Elixir, each function belongs to a module, which is just a namespace that lives in its own file. You just call a function with the module prefix, like
MyApp.Accounts.register_user(“me@example.com”)
There’s no inheritance, though there is polymorphism via something called Protocols. This makes it trivial to find the actual code you’re executing, which makes it so easy to debug stuff.
There are primitive data types, like integers, floats, and binary blobs (and strings are just binaries that are expected to be UTF-8), and then simple data structures like lists and maps. You can define structs, which are just maps with keys you define at compile-time.
I find that this leads to code that is way, WAY easier to design, write, read, and debug. I’m never stressing over trying to find the perfect abstraction for whatever I’m trying to write. I just write the function that does the thing I want. And you don’t need to remember a hundred different “design patterns,” either. There are a few simple patterns like map and reduce, and those are still just functions that transform data.
Ok I'm not that into programming yet, what is a namespace? I've seen it in some C code, where it says "using namespace std" for some IO stuff like cout and cin.
I’m using in the generic sense, as a bucket of function names. It’s kind of like how a class is a namespace for the methods defined on it. Two different classes can have a method with the same name, but you can’t define two methods with the same name & same args on one class.
I still use C for embedded device programming and it's really just about splitting code into separate files by what they do if an app ever gets too big.
If you really need something OOP'ish, you can use function pointers inside of a struct. You would be lacking the OOP syntax of C++, but it's fundamentally similar.
When you are counting bytes for firmware, it's helpful to have a language like C. In theory, it limits code complexity and is much easier to estimate what is going to be shat out of the compiler. Honestly, byte counting is super rare for me since there is just so much program space on devices these days. (If I did any work with ATTiny MCUs, I would probably coding in .ASM anyway...)
While I don't code in Rust (yet), I still think it makes perfect sense not to leverage classes. My limited experience in *lang languages taught me that simple functions are perfect for heavy parallelization. By restricting the number of pointers you are tossing around and using only immutable values, the surface area for failure is drastically reduced. (This is also awesome for memory safety as well.)
Just remember that all languages are tools and you should use the tools that fit the job. Efficiency should always be top of mind and not the nuances of a language. (I grew up learning how to conserve CPU ticks, so that should explain my point of view.)
I use helix as my editor. It's vim-like and great for rust right out of the box with no configuration. So much so that it replaced my 300+ line 20+ plugin neovim configuration with 1 line of toml (to set the theme). It's also written in rust :3
My dear friend - what if I told you that every call to Player.move should return an entirely new instance of a Player? One with an immutable position, and a helper function that takes a position delta - and constructs yet another Player!
What if I told you that all user interfaces are a function of application state; and all interactions apply a transformation that is then re-rendered? (We have gotten very good at only re-rendering the parts that change.)
Welcome to FP! There’s a whole world here for you to explore. You’ll be telling your friends about monoids and endofunctors before you know it :)
If you're used to it, it works fine. The thing is that everything is immutable, so having tons of copies doesn't matter in terms of incorrect state between threads (just don't actively hold onto old copies).
Performance certainly does suffer to some degree, but that's single-core performance. Where FP really shines is that you can very easily parallelize everything.
Not as “horrifying” as you make it sound. However, it does rely quite a bit on compiler optimizations. Haskell uses this approach a lot: Rust, however, very rarely does.
Constricting a new player for every movement seems a bit excessive and slow because of memory, but I'm on board. Our teacher always told us that hoping around in memory is bad tho
Lol this is the wrinkle - FP is great for humans, but under the hood, what is memory but a big block of mutable state? Sometimes we have to dig into the specifics for performance.
That being said - Rust knows that the instance of Player passed in is the only reference, and it can re-use that memory - maybe even mutate it in-place ;) while still presenting an FP interface to the user.
I only use C when I very likely don't need classes, and if I then still need to, I can fake them more or less well with structs, functions and pointers to functions in structs.
Absolutely no problem. I've done decades of programming in C, and it's absolute fine. For a bigger project, you need discipline, yes, but there are bigger projects in C out there that prove that this can be done.
Actually, my error rate is way below that of my coworkers who do C++ and C#, despite that I'm working directly on the iron (i.e. there is no OS between me and the processor, no interprocess protection, or similar).
C programmer here. I can't code in Rust and although I do have some interest in learning it, C is still the best one to me. Probably not the best way to do it, but I'd do something like this (based on the code in your ss):
I would probably move the struct Player and the functions player_new, player_move and player_rotate to another file (like player.c or sth), I'd create its respective header file with the definitions of each thing and with that I basically created a simple interface to create and handle players without needing OOP. English is not my native language, so I'm not really sure about what's the name of the programming paradigm used in C (which definitely is not OOP), but in my native language we call it "programación estructurada", which in English would be something like "structured programming".
Tbh I code in both non-OOP and OOP languages (most of the time C, JS and Python) and to me both paradigms are just fine. You can pretty much do anything in either, but each of them might work better for you on different situations and depending on your needs. I also use Vim btw.
@autumn64@lemmy.blahaj.zone
Eso en C se llama TDA (tipos de datos abstractos) creas una estructura que contiene los datos y luego un "set" de funciones que manejan todo lo de esa estructura. Es como un cuasi OO en C. La estructura tiene los atributos y las funciones específicas para esa estructura serían los métodos. @Smorty@autumn64@mast.lat
I'm sorry but this is effectively just OOP but worse.
You're still defining methods of the player class here but the referenced object/struct is explicit rather than implicit. Contrary to languages that properly support OOP though, they're entirely separated from each other and entirely separate from the data type they effectively represent methods of as far as the language is concerned. They only share an implicit "namespace" using the player_ function name prefix convention which is up for humans to interpret and continue.
There's still quite a few software written in C that does exactly as I did though. Look at OpenSSL's EVP library. I'm not sure about what you mean by "OOP but worse", wouldn't everything be worse than OOP since C isn't an OOP language?
Anyways. As I said, what I did is way more common than it seems at least in C, so I get your point but still I can't seem to be able to see what's inherently wrong with it. I would appreciate if you shared any better ideas you might have, though!