Skip Navigation

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

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

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

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

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

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

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

62 comments