Exploring Rust’s Structs, Traits, Enums, and Collections

A Node.js Developer’s Guide about Rust

--

In our previous article, we discussed Rust powerful type system from a Node.js developer’s perspective. You can find the article here:

Building upon our previous article, which introduced Rust’s powerful type system, in this article, we’ll dive into Rust’s core features: structs, traits, enums, and collections, with examples and comparisons to Node.js.

Structs

In Rust, structs are custom data types that allow you to group together related pieces of data. They’re similar to JavaScript objects in terms of functionality but have a different syntax.

Rust Struct Example:

struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

fn main() {
let user1 = User {
email: String::from("user@example.com"),
username: String::from("username123"),
active: true,
sign_in_count: 1,
};
}

Node.js Object Example:

class User {
constructor(email, username, active, signInCount) {
this.email = email;
this.username = username;
this.active = active;
this.signInCount = signInCount;
}
}

const user1 = new User("user@example.com", "username123", true, 1);

In the Rust example, we define a User struct with fields and their respective types. In the main function, we create an instance of the User struct by providing values for each field. In the Node.js example, we define a User class with a constructor and create an instance using the new keyword.

Traits

Traits in Rust are similar to interfaces in other languages, allowing you to define shared behavior across different types. They’re useful for implementing polymorphism and can be compared to JavaScript’s prototype-based inheritance.

Rust Trait Example:

trait Speak {
fn speak(&self);
}

struct Human {
name: String,
}

struct Dog {
name: String,
}

impl Speak for Human {
fn speak(&self) {
println!("Hello, my name is {}.", self.name);
}
}

impl Speak for Dog {
fn speak(&self) {
println!("Woof! My name is {}.", self.name);
}
}

fn main() {
let human = Human { name: String::from("Alice") };
let dog = Dog { name: String::from("Buddy") };

human.speak();
dog.speak();
}

Node.js Interface Example:

class Speakable {
speak() {
throw new Error("speak method not implemented");
}
}

class Human extends Speakable {
constructor(name) {
super();
this.name = name;
}

speak() {
console.log(`Hello, my name is ${this.name}.`);
}
}

class Dog extends Speakable {
constructor(name) {
super();
this.name = name;
}

speak() {
console.log(`Woof! My name is ${this.name}.`);
}
}

const human = new Human("Alice");
const dog = new Dog("Buddy");

human.speak();
dog.speak();

In the Rust example, we define a Speak trait with a single method, speak. We then define two structs, Human and Dog, and implement the Speak trait for each of them. In the Node.js example, we achieve a similar result using class inheritance.

Enums

Enums in Rust allow you to define a type that can represent one of several variants. This is useful for creating data structures that can hold different types of data. In JavaScript, you might achieve similar functionality using tagged unions or discriminated unions.

Rust Enum Example:

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

fn process_message(message: Message) {
match message {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to red: {}, green: {}, blue: {}", r, g, b),
}
}

fn main() {
let message = Message::Write(String::from("Hello, Rust!"));
process_message(message);
}

Node.js Discriminated Union Example:

class Message {
constructor(type) {
this.type = type;
}
}

class Quit extends Message {
constructor() {
super("Quit");
}
}

class Move extends Message {
constructor(x, y) {
super("Move");
this.x = x;
this.y = y;
}
}

class Write extends Message {
constructor(text) {
super("Write");
this.text = text;
}
}

class ChangeColor extends Message {
constructor(r, g, b) {
super("ChangeColor");
this.r = r;
this.g = g;
this.b = b;
}
}

function processMessage(message) {
switch (message.type) {
case "Quit":
console.log("Quit");
break;
case "Move":
console.log(`Move to x: ${message.x}, y: ${message.y}`);
break;
case "Write":
console.log(`Write: ${message.text}`);
break;
case "ChangeColor":
console.log(`Change color to red: ${message.r}, green: ${message.g}, blue: ${message.b}`);
break;
}
}

const message = new Write("Hello, Node.js!");
processMessage(message);

In the Rust example, we define an enum Message with four variants, each representing a different kind of message. We then create a process_message function that uses pattern matching to handle each variant. In the Node.js example, we use a base class Message and create subclasses for each variant, using the type property for discriminated union functionality.

Collections

Rust offers several built-in collection types for storing and organizing data, such as Vec (a growable array), HashMap (a key-value store), and HashSet (a set of unique values). In Node.js, you can use native data structures like Array, Map, and Set for similar purposes.

Rust Collections Example:

use std::collections::{HashMap, HashSet};

fn main() {
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.push(3);

let mut hashmap = HashMap::new();
hashmap.insert(String::from("Blue"), 10);
hashmap.insert(String::from("Yellow"), 20);

let mut hashset = HashSet::new();
hashset.insert(1);
hashset.insert(2);
hashset.insert(3);
}

Node.js Collections Example:

const array = [1, 2, 3];

const map = new Map();
map.set("Blue", 10);
map.set("Yellow", 20);

const set = new Set([1, 2, 3]);

In the Rust example, we import and create a Vec, a HashMap, and a HashSet from the standard library. Afterward, we insert values into each of these collections. Meanwhile, the Node.js example demonstrates how to achieve similar functionality using native data structures like Array, Map, and Set.

Conclusion

Throughout this article, we have explored some of Rust’s essential features, including structs, traits, enums, and collections, by drawing comparisons to Node.js concepts. Our aim was to provide you with a deeper understanding of Rust’s inner workings and how you can harness its features effectively. As a Node.js developer, learning Rust can broaden your perspective, improve your problem-solving skills, enhance performance, and enable you to write safer and more concurrent code. Happy coding!

--

--

Giuseppe Albrizio
Rustified: JavaScript Developers’ Odyssey

Graduated in sound engineering and working as full-stack developer. I’ve spent these years in writing tons of line of codes and learning new things every day.