this post was submitted on 18 Mar 2024
83 points (98.8% liked)

Transprogrammer

818 readers
1 users here now

A space for trans people who code

Matrix Space:

Rules:

founded 1 year ago
MODERATORS
 

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 are viewing a single comment's thread
view the rest of the comments
[–] [email protected] 31 points 8 months ago (2 children)
impl Player {
    fn move(&mut self, x: f64, y: f64) { ... }
}

player.move(10.0, 0.0);
[–] [email protected] 6 points 8 months ago (1 children)

Where would we define the player position?

[–] [email protected] 9 points 8 months ago (1 children)

You'd use a struct like

struct Player {
    x: f64,
    y: f64,
}
[–] [email protected] 2 points 8 months ago (3 children)

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?

[–] [email protected] 5 points 8 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] 1 points 8 months ago (1 children)

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.

[–] dukk 1 points 8 months ago (1 children)

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

[–] [email protected] 1 points 8 months ago

I'm sure you're right - I wrote this on my phone.

[–] [email protected] 1 points 8 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.)

[–] [email protected] 6 points 8 months ago (1 children)

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

[–] [email protected] 12 points 8 months ago* (last edited 8 months ago) (1 children)

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.

[–] [email protected] 3 points 8 months ago (1 children)

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?

[–] [email protected] 2 points 8 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);
    }
}