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

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

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

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

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

  • 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!

  •  Turun   ( @Turun@feddit.de ) 
    link
    fedilink
    5
    edit-2
    6 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.

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

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

        • Ok fair. Didn’t think about that. I always think that for these kinda of low level optimisations one needs to tell the computer -hey, I’m creating a new object which has the same type and most parameters are the same BESIDES this one variable-

          • Well, if your compiler knows the old value is unused afterwards, it can just modify the parameters you want and return the object as is. And if you’re really manipulating non-mutable objects, shallow copies should be enough to replicate an object + a few modifications. I don’t have everything in mind but once the semantics of your program has been checked by the compiler and it starts emitting lower-level code, it can cheat a lot to optimize things out

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

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

  • 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

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

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

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

    • 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!

        • 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 ;)

          • 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! ;)