Skip to content

Instantly share code, notes, and snippets.

@mculp
Last active January 19, 2023 07:32
Show Gist options
  • Save mculp/5ccdecfaa7300367ce3a7fbde7a80cef to your computer and use it in GitHub Desktop.
Save mculp/5ccdecfaa7300367ce3a7fbde7a80cef to your computer and use it in GitHub Desktop.
How to learn Rust as a Ruby developer

Intro to Learning Rust as a Ruby developer

Rust is a systems programming language that is designed to be safe, concurrent, and fast. It is similar to C++ in terms of its low-level capabilities, but with a number of features that make it more memory-safe and easier to use. Here is a simple example of a "Hello, World!" program in Rust:

fn main() {
    println!("Hello, World!");
}

This code defines a function called main, which is the entry point of every Rust program. The println! macro is used to print a string to the console. The exclamation mark indicates that this is a macro, as opposed to a function.

Here are some Rust concepts that might be new to you as a Ruby developer:

  • Ownership: Rust has a unique concept of ownership, which is used to manage memory and prevent data races. Each value in Rust has a variable that is considered its owner. The owner has the ability to read and write the value, but once the owner goes out of scope, the value is dropped.
  • Borrowing: Rust allows you to borrow a value from its owner, so that you can read or write it without taking ownership.
  • Lifetime: Every borrowed value has a lifetime, which is the scope for which it is valid. A lifetime parameter is added to the function signature to indicate the lifetime of the borrowed value
  • Patterns: Rust has powerful pattern matching capabilities, similar to those found in functional languages such as ML and Haskell. Patterns are used in match expressions, which are similar to switch statements in C-like languages.
  • Traits: Traits are similar to interfaces in other languages, and they provide a way to define common behavior that can be shared across multiple structs and enums.

Rust has a steep learning curve, but its unique features make it well suited for systems programming, and it's a language that is gaining popularity for its safety, performance, and concurrency. It's been voted most-loved programming language on Stack Overflow for the past 7 years!

Rust has its own package manager called cargo, that makes it easy to manage dependencies and build your projects. It has great documentation and a large number of libraries to make it easy to start coding with Rust. Rust is also well known for its helpful and friendly community, so don't hesitate to reach out for help or guidance.

I bet you're wondering, hmm.. what is a macro?

Macros in Rust can be thought of as a form of metaprogramming. They allow you to write code that generates code at compile-time, as opposed to runtime. This allows you to write code that is more expressive and more efficient than what you could write by hand.

macro_rules! is a built-in macro in Rust that allows you to define your own macros. It is used to define a new macro with a given name and a set of rules for how the macro should expand. The syntax of the rules is similar to that of a match expression, and it allows you to specify different expansions for different inputs.

Think of them as the "power tools" of Rust. Just like how you might use a power drill or a saw to make a woodworking project faster and more efficient, or use metaprogramming to build a library in Ruby, macros in Rust allow you to write code that writes code for you.

Here's an example of a simple macro that generates a struct (a Class in Ruby) with a specified number of fields:

macro_rules! struct_fields {
    ( $( $x:ident: $t:ty ),* ) => {
        struct Example {
            $(
                $x: $t,
            )*
        }
    };
}

struct_fields!(field1: i32, field2: String, field3: f32);

This macro takes a list of fields, each specified with an identifier and a type, and generates a struct (read: Class) with those fields. Funny enough, this is essentially a compile-time OpenStruct in Ruby.

It may look overwhelming at first, but let's break it down.

The $( $x:ident: $t:ty ),* syntax is called a pattern, and it is used to match the list of fields. In Ruby 2.7, pattern matching was introduced, and this is Rust's syntax for pattern matching.

The $x and $t are called meta-variables, and they capture the identifier and type of each field. The * after the pattern indicates that the pattern should match zero or more times.

You can see how this macro can make it very easy to generate structs with many fields, without having to write out each field individually. This can be especially useful when working with data that is generated by some other process, such as data that is read from a file or a network connection.

Comparing classes and structs

In Rust, classes are called structs and they are similar to classes in Ruby in that they are used to define objects with properties and methods. However, there are a few key differences:

  • Structs in Rust are generally used for data storage, whereas classes in Ruby are used for both data storage and behavior.
  • Rust defines classes using the struct keyword and their properties are called fields.
  • Rust defines methods using the impl keyword, which is attached to one or many structs.

Rust structs have a couple parallels with Ruby's built in Struct class:

  1. They both have a fixed number of properties
  2. They are used as lightweight containers for data
struct Point {
    x: i32,
    y: i32,
}
Point = Struct.new(:x, :y)

Impl

In Ruby, methods are defined inside the class itself. In Rust, methods are defined using the impl keyword and attached to a class. One way to think about Rust's impl is that it's similar to a Ruby Module. For example, an impl block in Rust that defines methods for the Point struct is similar to a module in Ruby that contains methods that are included in the Point class.

struct Point {
    x: i32,
    y: i32
}

impl Point {
    fn distance(&self, other: &Point) -> f32 {
        let x_squared = (self.x - other.x).pow(2);
        let y_squared = (self.y - other.y).pow(2);
        (x_squared + y_squared).sqrt()
    }
}
class Point
  attr_accessor :x, :y
  include PointDistance
end

module PointDistance
  def distance(this = self, other)
    x_squared = (this.x - other.x) ** 2
    y_squared = (this.y - other.y) ** 2
    Math.sqrt(x_squared + y_squared)
  end
end

Traits WIP

A Rust trait is similar to a Ruby 3 rbs file in that it is a construct that contains method signatures with type information. For example, a trait in Rust called Summable that defines a method add with the types of the arguments and the return value is similar to a Ruby 3 rbs file that defines the types of the arguments and the return value of the add method.

Enums WIP

A Rust enum is similar to a Ruby constant array -- they both can also have methods mixed in from an impl or Module. For example, an enum in Rust called Color that defines variants for Red, Green, Blue and also has methods mixed in from an impl block is similar to a constant array in Ruby that defines constants for Red, Green, Blue and also has methods mixed in from a module.

Built-in types and keywords

Ayy, let me break it down for you, my G. In Rust, we got a bunch of different types and keywords that we use to make our code fly like a private jet.

First up, we got the basic types:

  • i8, i16, i32, i64, i128 for signed integers
  • u8, u16, u32, u64, u128 for unsigned integers
  • f32, f64 for floating-point numbers
  • bool for booleans
  • char for characters
  • str for strings

Next, we got the compound types:

  • arrays for fixed-size arrays
  • vectors for dynamic arrays
  • tuples for fixed-size collections of different types
  • structs for custom types with named fields
  • enums for custom types with a fixed set of variants

We also got some keywords that allow us to control the flow of our code:

  • if, else, else if for control flow
  • for, while, loop for loops
  • break, continue, return for flow control
  • match for pattern matching
  • let, const for variable bindings

And lastly, we got the keywords that allow us to organize our code:

  • fn for defining functions
  • mod for defining modules
  • trait for defining traits
  • impl for implementing traits and other functionality
  • use for importing functionality
  • pub for making functionality public
  • as and rename for controlling names when importing

All of these types and keywords work together to make Rust one of the most powerful and safe languages out there, and with them, you can make your code fly like a private jet, and make money like a Cash Money Millionaire.

That's all I got for you, let me know if you need anything else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment