this post was submitted on 21 Jun 2024
5 points (100.0% liked)

Learning Rust and Lemmy

231 readers
1 users here now

Welcome

A collaborative space for people to work together on learning Rust, learning about the Lemmy code base, discussing whatever confusions or difficulties we're having in these endeavours, and solving problems, including, hopefully, some contributions back to the Lemmy code base.

Rules TL;DR: Be nice, constructive, and focus on learning and working together on understanding Rust and Lemmy.


Running Projects


Policies and Purposes

  1. This is a place to learn and work together.
  2. Questions and curiosity is welcome and encouraged.
  3. This isn't a technical support community. Those with technical knowledge and experienced aren't obliged to help, though such is very welcome. This is closer to a library of study groups than stackoverflow. Though, forming a repository of useful information would be a good side effect.
  4. This isn't an issue tracker for Lemmy (or Rust) or a place for suggestions. Instead, it's where the nature of an issue, what possible solutions might exist and how they could be or were implemented can be discussed, or, where the means by which a particular suggestion could be implemented is discussed.

See also:

Rules

  1. Lemmy.ml rule 2 applies strongly: "Be respectful, even when disagreeing. Everyone should feel welcome" (see Dessalines's post). This is a constructive space.
  2. Don't demean, intimidate or do anything that isn't constructive and encouraging to anyone trying to learn or understand. People should feel free to ask questions, be curious, and fill their gaps knowledge and understanding.
  3. Posts and comments should be (more or less) within scope (on which see Policies and Purposes above).
  4. See the Lemmy Code of Conduct
  5. Where applicable, rules should be interpreted in light of the Policies and Purposes.

Relevant links and Related Communities


Thumbnail and banner generated by ChatGPT.

founded 9 months ago
MODERATORS
 

Finally, we can make our own types (or data structures)!!


This is supplementary/separate from the Twitch Streams (see sidebar for links), intended for discussion here on lemmy.

The idea being, now that both twitch streams have read Chapters 5 and 6, we can have a discussion here and those from the twitch streams can have a retrospective or re-cap on the topic.

This will be a regular occurrence for each discrete set of topics coming out of The Book as the twitch streams cover them


With Ch 4 on the borrow checker out of the way, chapters 5 & 6 feel like the "inflection point" ... the point where we're ready to actually start programming in rust.

Custom types, data structures, objects with methods, pattern matching, and even dipping into rust's traits system and it's quasi answer to class inheritance.

If you're comfortable enough with the borrow checker, you can really start to program with rust now!


I personally didn't think this content was difficult, though it prompts some interesting points and topics (which I'll mention in my own comment below).

  • Any thoughts, difficulties or confusions?
  • Any quizzes stump you?
  • Any major tips or rules of thumb you've taken away or generally have about using structs and enums?
top 8 comments
sorted by: hot top controversial new old
[–] [email protected] 4 points 4 months ago (1 children)

For me, the biggest things to take away from these chapters were:

  • Enums and pattern matching
  • Borrow checker concerns emerging from these new data structures
  • Derivable traits

Enums and pattern matching for the win

  • That the pattern matching facility is powerful, and that enums can have associated data, structured independently for each variant ... really provides a (relatively straight forward and versatile "happy path" in rust IMO.
    • I tried to hack together a file differ in rust a couple of months ago (see my post on my solution here, mostly in the comments) and found myself just leaning into enums + pattern matching and rather enjoying the process.
  • So much so that major (and celebrated?) features of the language such as Option and Result types are really just applications of enums (along with rust's good type system)

The example in the book of the IP Address enum type is quite a nice demonstration I think:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

We're still learning "The Borrow Checker"

  • Ownership and borrowing concerns are still alive here in their application to structs and enums and what best practices arise out of it all.

Match statements

  • In match statements, the concerns are relatively straight forward (I think). Match arms take ownership of the variables they "use/touch" (I'm still unclear on the details there!) ...
  • so if you want a variable to live beyond the match statement, match on a reference.

EG:

let opt: Option<String> = Some(String::from("Hello world"));

match &opt {
    Some(s) => println!("Some: {}", s),
    None => println!("None!")
};

println!("{:?}", opt);
  • There's a slightly tricky thing that happens implicitly here:
    • Though the match is on &opt, the s in the pattern Some(s) is also a reference because rust implicitly "pushes down" the reference from the outer enum to the inner field or associated data.
    • Seems tricky, but also ergonomically sensible.

Borrowing self in methods

Probably the trickiest and most relevant part of the two chapters

  • the self in methods, like any other variable, can be one of three types in terms of ownership:
    • Owned by the method, like a plain variable
    • A reference (&self)
    • A mutable reference (&mut self)
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn set_width(&mut self, width: u32) {
        self.width = width;
    }

    fn max(self, other: Rectangle) -> Rectangle {
        Rectangle { 
            width: self.width.max(other.width),
            height: self.height.max(other.height),
        }
    }
}
  • What's tricky about this is that a method's signature for self has consequences that both reach back to the initial type of the root object (ie, is it mutable or not) and forward to what can be done with the root type afterward.

    • EG, a method that takes &mut self can't be used on a variable that isn't initially mutable.
    • EG, a method that takes ownership of self effectively kills the root object, making it unusable after the method is called!!
  • I'm sure there are a bunch of patterns that emerge out of this (anyone with some wisdom here?) ...

  • But the simple answer seems to borrow self, and if necessary, mutably borrow.

  • Taking ownership of self is an interesting way to enforce a certain kind of usage and behaviour though.

  • As the object dies, the natural return of an owning method would be a new object, probably of the same type.

  • Which leads into a sort of functional "pipe-line" or "method chaining" style of usage, not unlike the "Faux-O" idea in Cory Bernhardt's talk Boundaries. It's likely not the most performant, but arguably has some desirable qualities.

Derivable Traits

  • We haven't gotten to traits yet, but they come up here.
  • Turns out rust has kinda has a system of inheritance for structs where a trait can be easily implemented for a struct "automagically": #[derive(Debug)]

EG:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
  • This particular trait, Debug, allows for the printing of a struct's full makeup with println!.

  • All of the "Derivable" traits (from the std lib) are listed in Appendix C of The Book

  • There aren't that many, but they're useful:

    • Copy and Clone enable a struct to be copied without having to worry about ownership (though you have to be careful about the types of the fields, as its their copy methods that are ultimately relied on)
    • Four traits that implement methods for comparison and equality operators
    • Hash for hashing an object
    • Default for defining default values
  • Of course, when we cover traits we'll learn how to implement them ourselves for our custom types, but these seem to be fundamental features of the language, and easy enough to use right away.

[–] [email protected] 2 points 4 months ago* (last edited 4 months ago) (1 children)

I’m sure there are a bunch of patterns that emerge out of this (anyone with some wisdom here?) …

The classical one is something that looks like the following:

struct LoggedOut;
struct User {name: String};
struct Admin {name: String};

impl LoggedOut {
  fn login(self, name: String, password: String) -> User {
    User { name }
  }
  fn admin_login(self, name: String) -> Admin {
    Admin { name }
  }
}

impl User {
  fn log_out(self) -> LoggedOut {
    LoggedOut {}
  }
}

impl Admin {
  fn log_out(self) -> LoggedOut {
    LoggedOut {}
  }
}

fn fetch_user_preferences(user: User) { /*...*/ }

fn do_admin_action(admin_account: Admin) { /* ... */ }

fn main() {
  let mut user_session = LoggedOut {};
  /* (get user input) */
  match input {
    "login" => {
        user_session = user_session.login(name, password);
    }
    "admin" => {
       user_session = user_session.admin_login(name);
    }
  }
}

This would prevent you from writing code that uses the user's info / session "object" after they have logged out (and/or before they have logged in). On its own it's naive and a bit encumbering - I expect an enum would make more sense but then you can't restrict via types which user session values can and can't be passed to specific functions. In any case, when you are building a whole system around it it can be very useful to have the compiler enforcing these sorts of things "in the background".

This is basically what Gary Bernhardt is talking about in the talk you linked.

[–] [email protected] 2 points 4 months ago

Yep. And then you realise that "move semantics" aren't just a safety net that you have to "fight with" but actually a language feature against which you can develop/deploy desirable patterns.

A minor thing I noted reading your code snippets was that I immediately noticed, like at a gestalt level, the lack of ampersands (&) and therefore references and could immediately tell that this was a "faux-O"/pipline style system. Not too bad for a syntax often derided as messy/bad.

[–] [email protected] 2 points 4 months ago (1 children)

I want to highlight one of the more subtle-yet-important clarifications made in these 2 chapters: associated functions vs methods, and how method calls are just syntactic sugar for function calls.

Unlike in many other languages, there is no formal distinction (e.g. via separate keywords) between methods vs constructors vs property getters. The first parameter as well as the return type determine if a given associated function is "actually" a constructor or a method (etc.).

Personally, I find this incredibly elegant; it's a form of "less is more" that gets out of my way when I'm coding while still allowing me to use all of the existing patterns that I know from elsewhere.

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

Personally, I find this incredibly elegant

I'm not entirely sure I understand exactly what you mean here.

Do you appreciate it as an implementation design for the language (I do too)?

Or do you see some utility in being able to call MyStruct::my_method(&my_var)

... or both, cuz there's something assuring in knowing the simple pattern underneath the syntactic sugar is there?

[–] [email protected] 2 points 4 months ago (1 children)

Bit of both, I suppose. Along with my own experience trying to deal with prototypes in JavaScript and how Python handles methods vs "bare" functions internally in terms of v-tables and "where" things exist in memory.

I imagine the fact that both of those are interpreted languages plays somewhat heavily into it.

With regards to being able to write MyStruct::my_method(&my_var), it's the one-two punch of "I can use that specific syntax to differentiate between 'inherited' methods that have the same name" and that the compiler doesn't treat .method() calls any differently and just rewrites them as such when doing it's job.

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

I imagine the fact that both of those are interpreted languages plays somewhat heavily into it.

Yea I'd imagine so too.

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

Also, on the topic of "having references as fields in structs" ... see this conversation we had here about that.

That conversation started from a post on the users . rust-lang forum, where the ultimate pithy and harsh conclusion about doing this was:

You’re not allowed to use references in structs until you think Rust is easy. They’re the evil-hardmode of Rust that will ruin your day.

😉

Use Box or Arc to store things in structs “by reference”. Temporary borrows don’t do what you think they do.