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
Nat (she/they) ( @zea_64@lemmy.blahaj.zone ) 21•1 year agoimpl Player { fn move(&mut self, x: f64, y: f64) { ... } } player.move(10.0, 0.0);
Sooo impl is some kind of kinda class? It can carry data and methods, and what can it not?
Where would we define the player position?
zaphod ( @zaphod@feddit.de ) 6•1 year agoYou’d use a struct like
struct Player { x: f64, y: f64, }
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?
Ephera ( @Ephera@lemmy.ml ) 1•1 year agoYou 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.) GissaMittJobb ( @GissaMittJobb@lemmy.ml ) 1•1 year agoThe 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.
dukk ( @dukk@programming.dev ) 1•1 year agoBTW: this example probably won’t compile.
(
get_field
takes ownership ofself
and drops it when it goes out of scope. To prevent this, use&self
instead). GissaMittJobb ( @GissaMittJobb@lemmy.ml ) 1•1 year agoI’m sure you’re right - I wrote this on my phone.
ThatFembyWho ( @ThatFembyWho@lemmy.blahaj.zone ) English7•1 year agoSomebody 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.
Riskable ( @riskable@programming.dev ) English7•1 year agoWhen you call
player.move()
are you mutating the state ofplayer
or are you really just logically attaching themove()
function to theplayer
object in order to keep your code… Logical?player
could actually be an interface to some object in an external database andmove()
could be changing a value in that database. Orplayer
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 ) 5•1 year agoThe 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.
Azzy ( @AzzyDev@beehaw.org ) 1•1 year agostd::ops::Add my beloved
remotelove ( @remotelove@lemmy.ca ) 4•1 year agoI 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.)
navigatron ( @navigatron@beehaw.org ) 4•1 year agoMy 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 :)
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
Camille ( @Camille@lemmy.ml ) 2•1 year agoYour compiler is allowed to optimize things tho
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-
Camille ( @Camille@lemmy.ml ) 1•1 year agoWell, 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
navigatron ( @navigatron@beehaw.org ) 2•1 year agoLol 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.
AVincentInSpace ( @AVincentInSpace@pawb.social ) English1•1 year agoYou know, I’d been meaning to try out functional programming languages one of these days, but you’ve just about cured it. That’s horrifying.
Ephera ( @Ephera@lemmy.ml ) 3•1 year agoIf 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.
dukk ( @dukk@programming.dev ) 1•1 year agoNot 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.
nicoleb ( @nicoleb@lemmy.blahaj.zone ) 4•1 year agoI 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
Treczoks ( @Treczoks@lemm.ee ) 2•1 year agoAbsolutely 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).
30p87 ( @30p87@feddit.de ) 2•1 year agoI 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.
PINKeHamton ( @PINKeHamton@lemmy.blahaj.zone ) 1•1 year agoi 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.
Autumn64 ( @autumn64@lemmy.blahaj.zone ) 1•1 year agoC 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 functionsplayer_new
,player_move
andplayer_rotate
to another file (likeplayer.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.
𝚝𝚞𝚝𝚎@𝙼𝚊𝚜𝚝𝚘𝚍𝚘𝚗:~$: _ ( @tute_avalos@rebel.ar ) 2•1 year ago@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 Atemu ( @Atemu@lemmy.ml ) 1•1 year agoI’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 ( @autumn64@lemmy.blahaj.zone ) 1•1 year agoThere’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 ( @Atemu@lemmy.ml ) 1•1 year agoThere’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 ( @autumn64@lemmy.blahaj.zone ) 1•1 year agoMy 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! ;)