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

  • @bwrsandman
    link
    44
    edit-2
    8 months ago

    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];
    

    Instead do

    struct Players {
        Vec2 positions[NUM];
        float rotations[NUM];
        vec4 colors[NUM];
        Sprites heads[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.

    • radix
      link
      fedilink
      68 months ago

      Is it possible for a particularly smart compiler to redo this all as if it were data-oriented? Sorry if that’s a silly question.

      • @bwrsandman
        link
        7
        edit-2
        8 months ago

        Not a silly question at all!

        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.

      • copygirl
        link
        fedilink
        English
        48 months ago

        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.

      • @bwrsandman
        link
        28 months ago

        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!

  • zea
    link
    fedilink
    308 months ago
    impl Player {
        fn move(&mut self, x: f64, y: f64) { ... }
    }
    
    player.move(10.0, 0.0);
    
        • Smorty [she/her]OP
          link
          fedilink
          18 months ago

          Oh. So we would have the methods and data in seperate parts? Or can we combine the Player impl and the Player struct and use them as one?

          • @mumblerfish
            link
            58 months ago

            The struct Player and impl Player works as a class, with the difference that the struct block defines the attributes, e.g. position, and the impl defines the methods, e.g. move operation, as you figured out.

            What Rust does not have is inheritence like you do for classes, instead you have traits.

            Say you have a vector class. You will need some objects of rotating vectors, some objects of translation vectors, and some objects that can do both rotation and translation. You do a subclass of this vector class for rotating vectors, you make another subclass for translating vectors… you know what? Maybe all rotating vectors have to be also translating vectors, because you sometimes need a subclass which needs both. Ok. Or something like that might be your solution.

            In Rust you’d instead define a rotation and translation trait, by saying structs dressed in this trait must have a method taking this and that argument, returning this and that. You make an impl block defining what these methods should look like for your vectors. You can dress one struct in several traits, so you have one struct which you give both the rotation and translation trait.

            The bonus with traits now is now that later you realize that, ah, not only my pure vectors needs to be able to rotate. My Car struct needs to rotate, or my Planet struct, whatever. Great, you have a trait you can give to those structs and it will have the same methods as your rotating pure vectors.

            I’m not a proper programmer, so this may all be a bit misleading. This is just how I think about it when I’m using Rust.

          • @[email protected]
            link
            fedilink
            18 months ago

            The Player-impl with a self-parameter can only be used together with a Player-struct.

            Example:

            
            struct Example {
                field: u8,
            }
            
            impl Example {
                fn new(field: u8) -> Self {
                    Self {
                        field,
                    }
                }
            
                fn get_field(self) -> u8 {
                    self.field
                }
            }
            
            // Usage
            let example = Example::new(1); // doesn't need an instance to be called
            
            let field = example.get_field(); // needs an instance to be called
            
            let field = Example::get_field(example); // equivalent to the previous call
            

            With reservations for that this code might not compile 100%. Anyway, I hope that clears it up.

            • @[email protected]
              link
              fedilink
              18 months ago

              BTW: this example probably won’t compile.

              (get_field takes ownership of self and drops it when it goes out of scope. To prevent this, use &self instead).

          • Ephera
            link
            fedilink
            18 months ago

            You cannot combine them, but you can simply write them below each other. It makes no difference.

            The biggest reason why they are in separate blocks, is because you can have multiple such impl-blocks, including in other files.

            This is, for example, really useful, if you’ve got a model data type that’s used in lots of places and you don’t want to put the de-/serialization logic for it into the same file where that data type is declared.
            You may not want that, because it’s ugly boilerplate code or because that de-/serialization logic require dependencies, which you don’t want to include everywhere where that model type is used. (The latter only becomes relevant for larger applications, which consist out of multiple sub-projects.)

    • Smorty [she/her]OP
      link
      fedilink
      58 months ago

      Sooo impl is some kind of kinda class? It can carry data and methods, and what can it not?

      • @quilan
        link
        11
        edit-2
        8 months ago

        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.

        • Smorty [she/her]OP
          link
          fedilink
          28 months ago

          So they are seperated into the method part and the data part, hm? Can we access them when giving them the same name? So if we have an impl and a struct of the same name, can use it the same? Like this: let mut player = Player(); player.move(vec2(10.0, 0.0)); player.position += vec2(10.0, 0.0); Or would this work differently?

          • zea
            link
            fedilink
            English
            28 months ago

            They’re separate blocks, but they’re talking about the same type. struct deals with the data, impl deals with associated functions/methods/constants. If you implement a trait, you’d write yet another block like impl Trait for Player { [the stuff required by the trait, like an interface]}

            trait Position {
                fn get_pos(&self) -> (f64, f64);
            }
            
            struct Player {
                x: f64,
                y: f64,
            }
            
            impl Player {
                const SOME_CONSTANT: usize = 42;
                fn not_associated_with_trait(&mut self) {
                    self.x += 1.0;
                }
            }
            
            impl Foo for Player {
                fn get_pos(&self) -> (f64, f64) {
                    return (self.x, self.y);
                }
            }
            
  • @loafty_loafey
    link
    238 months ago

    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.

  • @[email protected]
    link
    fedilink
    English
    108 months ago

    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.

  • Riskable
    link
    fedilink
    English
    98 months ago

    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.

    • Smorty [she/her]OP
      link
      fedilink
      48 months ago

      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!

  • magic_lobster_party
    link
    fedilink
    88 months ago

    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.

  • Wyre
    link
    English
    88 months ago

    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.

    • @[email protected]
      link
      fedilink
      18
      edit-2
      8 months ago

      You should just go back to C++ or whatever you’re comfortable with.

      I wouldn’t want to discourage people from learning new languages

        • Wyre
          link
          English
          -28 months ago

          You should of course do as you like. It’s just that if OO is a real feature you require then you should choose a language with that feature. But tbh you should chose a real OO language like Ruby.

          • Smorty [she/her]OP
            link
            fedilink
            38 months ago

            What’s not real about C++? I do want to use a compiled language for now, as I have toyed around with python and GDScript for a bit too long now. I wanna write some faaast code so I can do things unneccesarily quickly.

            • Riskable
              link
              fedilink
              English
              3
              edit-2
              8 months ago

              You want speed? Rust is a good choice. Probably the best choice based on the objective benchmarks and more subjective things like ease of making your code multi-threaded.

              Also, many would argue that “Rust is the future.” Now that I know Rust, C and C++ seem like old, crufty things that annoy me if I’m forced to use them 🤷

            • Wyre
              link
              English
              18 months ago

              It’s quite real but if you really wanted an OO experience you’d choose something like Ruby. Really check it out you may find you like it.

    • Kairos
      link
      fedilink
      148 months ago

      Methods are just functions that take in a struct self pointer.

      • Smorty [she/her]OP
        link
        fedilink
        28 months ago

        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.

        • Kairos
          link
          fedilink
          28 months ago

          That’s because Java and C don’t make it explicit. Python and Golang and others do.

        • Kairos
          link
          fedilink
          1
          edit-2
          8 months ago

          It’s also how inheritance works.

          Because struct members are just offsets of a memory address, to add more member types, you (as in the compiler) just create a new struct with the same offset for the inherited types and new, further down offsets for new member types.

  • @cosmicrose
    link
    English
    8
    edit-2
    8 months ago

    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.

    • Smorty [she/her]OP
      link
      fedilink
      28 months ago

      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.

      • @cosmicrose
        link
        English
        58 months ago

        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.

  • Turun
    link
    fedilink
    7
    edit-2
    8 months ago

    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.

  • @[email protected]
    link
    fedilink
    68 months ago

    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.)

  • @quilan
    link
    5
    edit-2
    8 months ago

    While at first, Rust’s lack of inheritance threw me off, I’ve found that traits do plenty of heavy lifting in that department.

    Edit: Also, you can make class-like accessors and functions with impl blocks. Is that what you mean?

  • @[email protected]
    link
    fedilink
    3
    edit-2
    8 months ago

    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

  • @[email protected]
    link
    fedilink
    28 months ago

    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).

  • Autumn64
    link
    fedilink
    18 months ago

    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):

    typedef struct Player{
          float pos_x;
          float pos_y;
          float rotation;
    } Player;
    
    Player player_new(){
          Player player;
          player.pos_x = 0.0;
          player.pos_y = 0.0;
          player.rotation = 0.0;
          return player;
    }
    
    void player_move(Player *player, float x, float y){
          player->pos_x += x;
          player->pos_y += y;
          return;
    }
    
    void player_rotate(Player *player, float by){
          player->rotation += by;
          return;
    }
    
    int main(int argc, char *argv[]){
          Player player1 = player_new();
          player_move(&player1, 10.0, 10.0);
          player_rotate(&player1, 180.0);
    
          return 0;
    }
    
    

    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.

    • Atemu
      link
      fedilink
      18 months ago

      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.

      • Autumn64
        link
        fedilink
        18 months ago

        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!

        • Atemu
          link
          fedilink
          18 months ago

          There’s still quite a few software written in C that does exactly as I did though.

          Oh, absolutely.

          ’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?

          I meant specifically this pattern you showed; it’s object-oriented programming but in a language that does nothing to support it.

          Rust isn’t an OO language either but it adds some sensible abstractions to make OOP-like programming possible in an immediately recognisable and standardised manner.

          I can’t seem to be able to see what’s inherently wrong with it

          My primary problem is that it’s convention rather than rule.

          I would appreciate if you shared any better ideas you might have, though!

          Just don’t use C if you can avoid it ;)

          • Autumn64
            link
            fedilink
            1
            edit-2
            8 months ago

            My primary problem is that it’s convention rather than rule.

            I agree, and thus I think it’s safe to assume we also hate the lack of encapsulation in Python despite technically being an OOP language.

            Just don’t use C if you can avoid it ;)

            I’m fine with C, and if I stop coding in C I would starve since my job depends on it, still thanks for the suggestion though! ;)

  • @[email protected]
    link
    fedilink
    17 months ago

    i don`t know i just never learned anything but C and asm. ps. i use doom emacs for its package manager over VIm or its flavors.