diff --git a/manifest.json b/manifest.json index b273d7c..aa2a437 100644 --- a/manifest.json +++ b/manifest.json @@ -3,11 +3,15 @@ "id": "rust_intro", "name": "Introduction to Rust", "upload": [ - "sections/00_rust_concepts" + "sections/00_rust_concepts", + "sections/01_rust_projects" ], "templates": "templates", "lessons": { "intro_to_rust": { + "next": ["rust_projects"] + }, + "rust_projects": { "next": [] } }, diff --git a/sections/00_rust_concepts/03_rust_syntax/README.md b/sections/00_rust_concepts/03_rust_syntax/README.md index 8c3669d..50c07bb 100644 --- a/sections/00_rust_concepts/03_rust_syntax/README.md +++ b/sections/00_rust_concepts/03_rust_syntax/README.md @@ -224,13 +224,10 @@ and using them gives us new ways we can express our code. ## Conclusion -Now that you understand some of Rust's basic syntax, it's time to move onwards. -You'll have access to a series of different projects, which will each walk you through different Rust concepts. -These are real-world projects, and will only interact with parts of Rust that are actually relevant to them. -So, pick the project that relates the most to what you want to use Rust for, -or just go with one that sounds interesting. -If you want to learn more, you can always come back and do another project -(hint: click the text below to view a "map" of every lesson to quickly navigate between them). +Now that you understand some of Rust's basic syntax, +there's just one more concept to cover before you can get to building some projects. +Next, we'll cover how to store data in rust using structs and traits +(which you can think of as the Rust equivalent of object-oriented programming). Also, take a look at some of the example code in the previous lesson to get a bit more practice with Rust's syntax, with this information in mind. Happy coding! \ No newline at end of file diff --git a/sections/00_rust_concepts/04_structs_and_traits/README.md b/sections/00_rust_concepts/04_structs_and_traits/README.md new file mode 100644 index 0000000..c28dce7 --- /dev/null +++ b/sections/00_rust_concepts/04_structs_and_traits/README.md @@ -0,0 +1,210 @@ +# Structs and Traits + +The last thing you need to learn before you can start really diving into Rust is how to properly store data. If you've ever used an object-oriented language before (Java, C#, etc.), then you can think of structs and traits as the Rust version of that. + +Structs are essentially ways to create little packets of data. For example, instead of writing functions that take in a user's data as individual parameters, like: + +```rust +fn user_function(id: u32, name: String, bio: String) { + // ... +} +``` + +We can package all this data up into a struct called `User` and use it like this: + +```rust +fn user_function(user: User) { + // ... +} +``` + +This can help make writing code a lot easier. Structs are a lot like classes in other languages, except without functions or methods. + +## Structs + +In order to create a struct, we need to define it like this: + +```rust +struct User { + id: u32, + name: String, + bio: String, +} +``` + +We start off with `struct`, then the name of the struct afterward. Then, we put all the struct's fields inside curly brackets. Each field is formatted as `name_of_field: DataType`. + +Note that, by default, structs are private. That means that they aren't accessible from other files/modules. In order to fix that, you can add a `pub` before `struct`: + +```rust +pub struct User { + id: u32, + name: String, + bio: String, +} +``` + +Struct fields are also private by default. That means that we won't be able to access them from outside files/modules. If we want to make them accessible, we can either create a method to get/set them, or make the fields themselves public: + +```rust +pub struct User { + pub id: u32, + pub name: String, + pub bio: String, +} +``` + +If we have a struct, we can access its fields using a dot syntax, similar to many other languages: + +```rust +my_user.id // Get the id field of my_user. +my_user.id = 10; // Set the id field of my_user. +``` + +And if we want to create an instance of a struct, we can use the following syntax: + +```rust +User { + id: 1, + // String literals have a type of &str, but + // the name field wants a String, so we need to + // use into() to convert it. + name: "User Name".into(), + bio: "Bio".into(), +} +``` + +## Tuple Structs + +We can also create a type of struct called a tuple struct. Instead of defining fields, we can define them like a tuple: + +```rust +struct UserTuple(u32, String, String); +``` + +And instead of accessing them by name, we access them by index: + +```rust +my_user_tuple.0 // Get the first (u32) field of the user. +``` + +Like with normal structs, the syntax for creating a new instance of them is very similar to how we define them: + +```rust +UserTuple(1, "User Name".into(), "Bio".into()) +``` + +It's worth noting that Rust also supports tuples that aren't structs. So, we can have a function that takes in `(u32, String, String)` directly: + +```rust +fn user_tuple_no_struct(user: (u32, String, String)) { + // Print out the first field in the tuple (u32). + println!("{}", user.0); + // ... +} + +user_tuple_no_struct((1, "User Name".into(), "Bio".into())); +``` + +Even though both contain exactly the same data, we aren't able to interchange them. So the following code wouldn't work: + +```rust +user_tuple_no_struct(UserTuple(1, "User Name".into(), "Bio".into())); +``` + +### Zero-Sized Structs + +Rust also has structs that contain no data at all. They are only useful in certain cases, so we'll only cover them briefly, but you can define them like this: + +```struct +struct NoData; +``` + +And create them by just using their names: + +```rust +NoData +``` + +## Struct Methods + +Even though structs don't have any methods in their definition, we can still add them another way. We'll ue the `impl` block: + +```rust +impl User { + pub fn id(&self) -> u32 { + self.id + } +} +``` + +This creates a function, called `id`, that takes in a reference to a `User` (`&self` means a reference to the thing that the method is implemented on, which in this case is a `User`) and returns a `u32`. If we have an instance of a `User`, called `my_user`, then we can call the function by doing `my_user.id()`. This means the same thing as `User::id(&my_user)` (which is just a different way to call these methods). + +If we wanted to modify data on the struct, we could create a function like this: + +```rust +pub fn set_bio(&mut self, bio: String) { + self.bio = bio; +} +``` + +The only difference here is that we take in a `&mut self` instead of a `&self` (because we need to have mutable access to the data if we want to modify it), and we take in a second parameter. To call this, we can use `my_user.set_bio("New Bio".into())`, which is the same thing as `User::set_bio(&mut my_user, "New Bio".into())`. + +Finally, we can write a function that doesn't take in an instance of the struct. This is useful if we want to write constructors: + +```rust +pub fn new(id: u32, name: String, bio: String) -> User { + User { + // This means the same thing as id: id, + // or setting the id field to the id variable + // above. + // Rust lets us collapse it if both have the same name. + id, + name, + bio, + } +} +``` + +## Traits + +If Rust structs are like classes without methods, then traits are like interfaces. They let us write "blueprints" for methods that need to exist on a class. We can write traits like this: + +```rust +trait HasDataID { + fn get_id(&self) -> u32; +} +``` + +This creates a trait called `HasDataID`. Inside the trait definition is all the methods that need to exist on the trait. If we have something that implements this trait, then it needs to have an `id` method that takes in a reference to itself and returns a `u32`. + +To implement this for user, we can write it as: + +```rust +impl HasDataID for User { + fn get_id(&self) -> u32 { + self.id + } +} +``` + +Then, we can write functions like this: + +```rust +pub fn print_id(value: impl HasDataID) { + println!("{}", value.get_id()); +} +``` + +This special syntax (`impl HasDataID`) means that the function can take in any piece of data that implements `HasDataID`. So, we could put a `User` in and it would work just fine. Behind the scenes, this is using generics (which we'll get into later), and it actually creates a copy of the function for each different kind of data that gets put into it. + +## Conclusion + +Now that you know how to work with data in Rust, it's time to move onwards. +You'll have access to a series of different projects, which will each walk you through different Rust concepts. +These are real-world projects, and will only interact with parts of Rust that are actually relevant to them. +So, pick the project that relates the most to what you want to use Rust for, +or just go with one that sounds interesting. +If you want to learn more, you can always come back and do another project +(hint: click the text below to view a "map" of every lesson to quickly navigate between them). +Happy coding! \ No newline at end of file diff --git a/sections/00_rust_concepts/04_structs_and_traits/config.json b/sections/00_rust_concepts/04_structs_and_traits/config.json new file mode 100644 index 0000000..6951493 --- /dev/null +++ b/sections/00_rust_concepts/04_structs_and_traits/config.json @@ -0,0 +1,4 @@ +{ + "defaultFile": "src/main.rs", + "source": "https://github.com/Cratecode/rust/tree/master/sections/00_rust_concepts/04_structs_and_traits" +} \ No newline at end of file diff --git a/sections/00_rust_concepts/04_structs_and_traits/manifest.json b/sections/00_rust_concepts/04_structs_and_traits/manifest.json new file mode 100644 index 0000000..02ad439 --- /dev/null +++ b/sections/00_rust_concepts/04_structs_and_traits/manifest.json @@ -0,0 +1,9 @@ +{ + "type": "lesson", + "id": "les_rust_structs_traits", + "extends": "basic", + "name": "Rust Structs and Traits", + "unit" : "rust_intro", + "spec": "An example of structs and traits in Rust.", + "class": "tutorial" +} diff --git a/sections/00_rust_concepts/04_structs_and_traits/src/main.rs b/sections/00_rust_concepts/04_structs_and_traits/src/main.rs new file mode 100644 index 0000000..8dcf6df --- /dev/null +++ b/sections/00_rust_concepts/04_structs_and_traits/src/main.rs @@ -0,0 +1,170 @@ +// This is a struct. +// It's a way to group data together into +// little packets. +// Structs are similar to classes in Java/C#, but +// they only contain data - no functions or methods. +// In order to save time, Rust has a way to +// "derive" implementations for certain traits (more +// on that later). +// The code "#[derive(Clone, Debug)]" +// is telling Rust to create implementations for the +// Clone and Debug traits for our struct. +// What this means is that, by implementing the +// Clone trait, we can run the `clone()` function on +// an instance of our struct to clone it. +// Deriving the Debug trait means that we can do +// something like println!("{user:?}") to +// print out an instance of User for debugging. +// The ":?" inside the print means to use debug printing. + +/// A User record, containing basic information about their account. +#[derive(Clone, Debug)] +pub struct User { + /// The user's unique ID. + id: u32, + /// The user's display name (username). + name: String, + /// The user's bio (information about them). + bio: String, +} + +// If we want to add methods to our struct, +// we can use an impl block. + +impl User { + // Functions inside the impl block can be called + // on instances of User. + // They can also be called like User::my_function(). + // Most of the time, the first argument of our + // function should be "&self", which means that + // the function takes in an **immutable** reference + // to the struct that it's implemented on. + // When we call a function like "user.id()", Rust + // will transform this into "User::id(&user)". + // So the first argument (self) ends up being the + // value that the function's called on. + + /// Returns the user's ID. + pub fn id(&self) -> u32 { + // self is a User struct, so we can + // access fields on it using this + // dot syntax, just like in Java/C#. + self.id + } + + // The function above didn't return a reference because + // it was returning a primitive. + // We actually could use a reference there, but it'd + // be all but pointless because a reference takes up + // about the same amount of memory as a number anyway, + // so we wouldn't be saving anything by doing it. + // Strings, on the other hand, can be quite large, so we + // might not want to create copies of them. + // Instead, we can return a reference, which can + // save memory and increase performance. + // If the code calling this does need to copy the + // String, then it can do itself like this: + // user.name().clone() + + /// Returns the user's name. + pub fn name(&self) -> &String { + &self.name + } + + /// Returns the user's bio. + pub fn bio(&self) -> &String { + &self.bio + } + + // Reading data from the struct isn't the only thing we can do here: + // we can also set it. + // To do that, we'll need a mutable reference to the struct, + // so instead of writing "&self", we'll write "&mut self". + // We'll also take in a value to set. + // This function can be called with user.set_bio(new_bio), + // which is the same thing as User::set_bio(&mut user, new_bio). + + /// Sets the user's bio to a new value. + pub fn set_bio(&mut self, bio: String) { + self.bio = bio; + } + + // Unlike in other languages, we don't need to create + // a constructor. + // But it's probably still a good idea to do so. + // By convention, we'll create a method called "new", + // but we can really call it anything. + // To call this, we'll use User::new(id, name, bio). + pub fn new(id: u32, name: String, bio: String) -> User { + // For the struct above, this syntax actually won't + // work in other files. + // This is because struct fields are **private by default**. + // In other words, from an outside file/module, we can't + // access the id, name, or bio fields on our struct. + // That means that we also can't set them, so we can't + // create a new instance of the struct. + // This can actually be a good thing, but if you want + // other files to be able to create a new value of + // the struct, you'll need to add "pub" before each of its + // fields above. For example, "pub id: u32". + User { + // This means the same thing as id: id, + // or setting the id field to the id variable + // above. + // Rust lets us collapse it if both have the same name. + id, + name, + bio, + } + } +} + +// We can implement traits like this. +// Here, we're implementing the Display trait for User. +// This trait lets us write things like println!("{my_user}"). +// Notice that this is different from the Debug trait above: +// they both do essentially the same thing, but the Debug trait +// is used for formatting a struct as a String for debug information, +// and the Display trait is used for formatting the struct as a String +// so it can be displayed to the user. +impl std::fmt::Display for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ID: {}\nName: {}\n============\n{}", self.id, self.name, self.bio) + } +} + +// Now, we can put everything together and create +// a function that takes users in and prints them out. +// This takes in a reference to a user because we don't need +// to take ownership of it to print it out. +// If we wrote "user: User", then everything would still work, +// but the code that called this function wouldn't be able +// to use its user anymore because ownership of it would be transferred +// into here. + +/// Prints a user to the console, with newlines before and +/// after the user. +pub fn print_user(user: &User) { + println!("\n{user}\n"); +} + +fn main() { + // We need to write into() to convert string literals, + // which have a type of &str, into a String. + // The differences between these two are subtle, but + // &str is a reference to a string, and String is + // an actual string. + let mut my_user = User::new(1, "Admin".into(), "Please contact webmaster@example.com if you notice any problems.".into()); + + println!("User's ID: {}", my_user.id()); + // We could also write println!("{:?}", my_user). + println!("Debug: {my_user:?}"); + + print_user(&my_user); + + my_user.set_bio("Maintenance in progress...".into()); + println!("New Bio: {}", my_user.bio()); + println!("Debug: {my_user:?}"); + + print_user(&my_user); +} \ No newline at end of file diff --git a/sections/00_rust_concepts/manifest.json b/sections/00_rust_concepts/manifest.json index 94e07e5..f1db45e 100644 --- a/sections/00_rust_concepts/manifest.json +++ b/sections/00_rust_concepts/manifest.json @@ -7,7 +7,8 @@ "00_rust_intro", "01_rust_ownership_borrowing", "02_rust_lifetimes", - "03_rust_syntax" + "03_rust_syntax", + "04_structs_and_traits" ], "lessons": { "les_rust_intro": { @@ -20,6 +21,9 @@ "next": ["les_rust_syntax"] }, "les_rust_syntax": { + "next": ["les_rust_structs_traits"] + }, + "les_rust_structs_traits": { "next": [] } } diff --git a/sections/01_rust_projects/010_mandelbrot_set_renderer/README.md b/sections/01_rust_projects/010_mandelbrot_set_renderer/README.md new file mode 100644 index 0000000..9d31de2 --- /dev/null +++ b/sections/01_rust_projects/010_mandelbrot_set_renderer/README.md @@ -0,0 +1,97 @@ +# Mandelbrot Set Renderer + +Rendering the Mandelbrot Set is one of my favorite projects because it's incredibly simple to create a simple renderer for it, but with enough effort you can improve it and create some astonishing results. Take a look! + +$$IMAGE MandelbrotFull.png An image of the Mandelbrot Set that is colorful, complex, and visually interesting$$ + +By the end of this series of projects, we'll build something capable of creating images like the one above. But first, let's start small. In the first lesson, there was an example of a simple Mandelbrot Set renderer - if you haven't already, [check it out](https://cratecode.com/lesson/rust-a-language-youll-love/75m1jc9k0p/xa3l5ahj5w)! This is what we'll be building today: + +$$IMAGE MandelbrotBasic.png An image of the Mandelbrot Set rendered as text with asterisks and spaces$$ + +## What is the Mandelbrot Set? + +Before we can build the renderer, we first need to understand what the Mandelbrot Set actually is. The point of this lesson is to learn Rust, so we won't get too deep into it, but if you're interested, check out [this Wikipedia article](https://en.wikipedia.org/wiki/Mandelbrot_set) on it. + +The Mandelbrot Set is a function that looks like this: + +$$f\left(z\right)=z^{2}+c$$ + +At its core, it's actually a really simple function, which makes it all the more incredible that it can create images like the one above. Of course, there's still a few more details to work out - namely, how we turn the function into an image on the screen. + +Let's start off with the two variables, `z` and `c`. Both of these are **complex numbers**. + +### Complex Numbers + +If you aren't familiar with them, that's alright! A complex number takes the form: + +$$z=a+bi$$ + +Where `a` and `b` are real numbers (the kind that you're used to) and `i` is the number $$\sqrt{-1}$$. That's a little weird, but all you need to care about is how to program them in. When you want to do math on them (like adding and multiplying them), you can just think of `i` as a variable. So, to add two complex numbers together, you'll do: + +$$z=a+bi$$ +$$k=c+di$$ +$$z+k=a+bi+c+di=\left(a+c\right)+\left(b+d\right)i$$ + +Multiplying is a bit similar, but you also need to use the fact that $$i^{2}=-1$$: + +$$z\cdot k=\left(a+bi\right)\cdot\left(c+di\right)=ac+\left(ad+bc\right)i+bdi^{2}$$ +$$z\cdot k=ac-bd+\left(ad+bc\right)i$$ + +If you're up for implementing these operations, I'd highly recommend it! However, if you don't want to, that's understandable as well. In that case, I'd recommend using the [num-complex](https://docs.rs/num-complex/latest/num_complex/) crate (which you can install by running `cargo add num-complex`), which can handle all of this math for you. + +If you do choose to implement it, here are a few hints: +* ||Use a struct to hold your complex numbers. You can store a real and imaginary part.|| +* ||You probably want to store your numbers as f32 or f64, so you can use decimals (f64 is more precise than f32).|| +* ||You can look at the source code for num-complex for some ideas.|| +* ||The first lesson has a full implementation of complex addition and multiplication.|| + +And if you're using `num-complex`, here are some hints for you as well: +* ||Use Complex32 or Complex64 for your numbers.|| +* ||You can create a new complex number like Complex64::new(1.0, 1.0).|| +* ||There are built-in functions for multiplying and adding complex numbers. For example, a.mul(b).|| + +Try implementing the function above. It should take in two values: `z` and `c`, and return +a new complex number. + +Here are some hints for doing it: +* ||To square a number, multiply it by itself. z^2 is z*z.|| + +### Displaying the Mandelbrot Set + +The Mandelbrot Set is the set (list) of all numbers where, for a number `c`, if we iterate the function on itself forever, starting with $$z=0$$, it doesn't go towards infinity. + +In practical terms, this means that, for an input number `c`, if we set `z` to zero, then run a bunch of iterations (i.e., 100) that set `z` to `f(z,c)`, if our new value for `z` isn't big (i.e., smaller than `2`), that number is in the set. In pseudocode: + +```javascript +let z = 0; +for (let i = 0; i < 100; i++) { + // z = z^2 + c + z = f(z, c); +} + +if (z < 2) { + print("In the set!"); +} else { + print("Not in the set."); +} +``` + +Note that, when dealing with complex numbers, if we want to check their size, we can't just use normal comparison operators, because they don't make sense for complex numbers. Instead, to check if they're super huge, we need to first get their absolute value (distance from zero), then check that. So `z < 2` above should really be `abs(z) < 2`. + +And, that's it. Of course, it's a bit more tricky to actually put it onto the screen (and we'll go over that), but this is all you need to write for the Mandelbrot Set side of things. The way we display it is by treating `c` as an xy-coordinate ($$x+yi$$). If it's in the set, we print out an asterisk (`*`), and if it isn't, we print out a space (` `). + +## Plotting + +Knowing how to display the Mandelbrot Set is good, but we still need a way to make it work with our console. Right now, we have a function that can take a point in the xy-plane and tell us whether to put an asterisk or a space there. So, all that's left to do is figure out how to map the text in our console to it. + +Luckily, this isn't too difficult to solve. Here's a simple approach: +* First, figure out your bounds in the xy-plane. I'd recommend going from `x = -1.5` to `x = 1.5`, and `y = 1` to `y = -1`. +* Next, figure out how many characters you're printing out. I'd recommend 90 characters wide and 24 characters down. +* Finally, start in the top-left corner `(-1.5, 1)` and write a loop (or maybe two) that can go to the bottom-right corner `(1.5, -1)`. + +Here are some hints for using this approach: +* ||Your outer loop should go across the y-axis.|| +* ||You can move by (y_final - y_initial) / height in each iteration of your outer loop.|| +* ||You can move by (x_final - x_initial) / width in each iteration of your inner loop.|| + +Good luck! \ No newline at end of file diff --git a/sections/01_rust_projects/010_mandelbrot_set_renderer/config.json b/sections/01_rust_projects/010_mandelbrot_set_renderer/config.json new file mode 100644 index 0000000..014cddb --- /dev/null +++ b/sections/01_rust_projects/010_mandelbrot_set_renderer/config.json @@ -0,0 +1,4 @@ +{ + "defaultFile": "src/main.rs", + "source": "https://github.com/Cratecode/rust/tree/master/sections/01_rust_projects/010_mandelbrot_set_renderer" +} \ No newline at end of file diff --git a/sections/01_rust_projects/010_mandelbrot_set_renderer/manifest.json b/sections/01_rust_projects/010_mandelbrot_set_renderer/manifest.json new file mode 100644 index 0000000..73b8c68 --- /dev/null +++ b/sections/01_rust_projects/010_mandelbrot_set_renderer/manifest.json @@ -0,0 +1,9 @@ +{ + "type": "lesson", + "id": "les_rust_mandelbrot_set_renderer", + "extends": "basic", + "name": "Rust Mandelbrot Set", + "unit" : "rust_intro", + "spec": "A Mandelbrot Set renderer written in Rust which creates a plot on the console.", + "class": "project" +} \ No newline at end of file diff --git a/sections/01_rust_projects/010_mandelbrot_set_renderer/src/main.rs b/sections/01_rust_projects/010_mandelbrot_set_renderer/src/main.rs new file mode 100644 index 0000000..5825b2f --- /dev/null +++ b/sections/01_rust_projects/010_mandelbrot_set_renderer/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, Cratecode!"); +} \ No newline at end of file diff --git a/sections/01_rust_projects/manifest.json b/sections/01_rust_projects/manifest.json new file mode 100644 index 0000000..8d0df55 --- /dev/null +++ b/sections/01_rust_projects/manifest.json @@ -0,0 +1,14 @@ +{ + "__comment": "This unit contains different projects to explore Rust, grouped by topic.", + "type": "unit", + "id": "rust_projects", + "name": "Rust Projects", + "upload": [ + "010_mandelbrot_set_renderer" + ], + "lessons": { + "les_rust_mandelbrot_set_renderer": { + "next": [] + } + } +} \ No newline at end of file