PONY λ M2 Modula-2

Rust.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Rust and Ruby

Rust 1.95 Ruby 4.0
Hello World & Running
Hello, World
fn main() { println!("Hello, World!"); }
puts "Hello, World!"
Ruby has no fn main() entry point, no println! macro, and no semicolons. Top-level code runs top to bottom the moment the file is loaded. puts writes a value followed by a newline — the everyday equivalent of println!.
Running a program
// Compile and run in one step: // cargo run // Build a release binary: // cargo build --release && ./target/release/myapp // No runtime needed to ship the binary.
# Run directly — no compile step, no binary: # ruby hello.rb # # One-liner without a file: # ruby -e 'puts "Hi"' # # Interactive REPL: # irb
Ruby is interpreted: there is no cargo build, no compiled binary, and no Cargo project structure needed for a script. ruby hello.rb parses and runs in one shot, and irb gives you an interactive REPL for quick experiments. The trade-off is that the Ruby interpreter must be present wherever the script runs.
Comments
fn main() { // Single-line comment /* Block comment */ let count = 0; // inline println!("{count}"); }
# Single-line comment =begin Block comment spanning several lines =end count = 0 # inline puts count
Ruby uses # for single-line comments instead of //. For block comments it uses =begin/=end, which must start in column one — there is no /* ... */ form. The # style is overwhelmingly preferred; =begin/=end is rare in practice.
Imports & requiring code
use std::collections::HashMap; fn main() { let mut scores: HashMap<&str, i32> = HashMap::new(); scores.insert("Alice", 42); println!("{:?}", scores); }
require "json" scores = { "Alice" => 42 } puts JSON.generate(scores)
Ruby's require loads a library by name and its constants and methods become available in the global namespace — no qualifier like std::collections:: is needed afterward. The standard library ships with Ruby, so there is no Cargo.toml and no package download for built-in libraries.
Variables & Types
Variables & mutability
fn main() { let name = "Ruby"; // immutable by default let mut counter = 0; // opt into mutability counter += 1; println!("{name} {counter}"); }
name = "Ruby" # mutable by default counter = 0 counter += 1 puts "#{name} #{counter}"
In Rust, bindings are immutable by default and require let mut to allow mutation. In Ruby, all variables are mutable by default — there is no let keyword and there is no way to declare a binding as immutable. Reassignment is always permitted, and the type can change on every assignment.
Constants
const PI: f64 = 3.14159; const GREETING: &str = "hello"; fn main() { println!("{PI}"); println!("{GREETING}"); }
PI = 3.14159 GREETING = "hello" puts PI puts GREETING
Ruby constants are any identifiers that begin with a capital letter, conventionally written in SCREAMING_SNAKE_CASE. Unlike Rust's const, Ruby constants are not truly immutable — reassigning one emits only a warning rather than a compile error, so they rely on convention rather than enforcement.
Type annotations vs. inference
fn main() { let count: i32 = 42; let ratio: f64 = 3.14; let label: &str = "hello"; let active: bool = true; println!("{count} {ratio} {label} {active}"); }
count = 42 ratio = 3.14 label = "hello" active = true puts "#{count} #{ratio} #{label} #{active}"
Ruby has no type annotations. Variables spring into existence on first assignment and can hold any object at any time — the interpreter determines the type at runtime. There is no equivalent of Rust's i32, f64, &str, or bool declarations, and the type can change freely on reassignment.
Destructuring assignment
fn main() { let (first, second, third) = (1, 2, 3); println!("{first} {second} {third}"); let numbers = [10, 20, 30, 40]; let [head, second_item, ..] = numbers; println!("head: {head}, second: {second_item}"); }
first, second, third = 1, 2, 3 puts "#{first} #{second} #{third}" numbers = [10, 20, 30, 40] head, second_item, *rest = numbers puts "head: #{head}, second: #{second_item}"
Both languages support destructuring assignment. Ruby's splat operator *rest captures remaining elements into an array, with no separate exhaustive vs. non-exhaustive matching — it simply takes whatever is left. The splat can appear anywhere in the left-hand side: first, *middle, last = array.
Shadowing vs. reassignment
fn main() { let value = 5; let value = value * 2; // new binding shadows the old one let value = value.to_string(); // can even change the type println!("{value}"); // "10" }
value = 5 value = value * 2 # plain reassignment value = value.to_s # type changes freely puts value # "10"
Rust allows shadowing — creating a new let binding with the same name, which can even change the type while keeping the old binding's value in scope during initialization. Ruby simply reassigns the variable: the semantics are the same for simple cases, but the underlying mechanics differ since Ruby has no binding stack.
Strings
One string type vs. two
fn main() { let borrowed: &str = "hello"; // string slice — stack let owned: String = String::from("world"); // heap buffer let combined = format!("{borrowed} {owned}"); println!("{combined}"); }
greeting = "hello" # always heap-allocated, always owned message = "world" combined = "#{greeting} #{message}" puts combined
Rust has two string types: &str (a borrowed slice, often a string literal) and String (an owned, heap-allocated buffer). Ruby has a single String type that is always heap-allocated and always owned. There is no borrowing distinction to track, and the garbage collector handles cleanup automatically.
String interpolation
fn main() { let name = "World"; let count = 3; println!("Hello, {name}!"); let message = format!("Repeated {count} times"); println!("{message}"); }
name = "World" count = 3 puts "Hello, #{name}!" message = "Repeated #{count} times" puts message
Ruby's string interpolation uses #{expression} inside double-quoted strings and can evaluate any Ruby expression inline — not just variable names. Rust's format! and println! macros use {name} or {} positional placeholders, which are checked at compile time but are macro calls rather than part of string syntax.
Common string methods
fn main() { let sentence = "the quick brown fox"; println!("{}", sentence.to_uppercase()); let words: Vec<&str> = sentence.split(' ').collect(); println!("{words:?}"); println!("{}", sentence.contains("quick")); println!("{}", sentence.replacen("quick", "slow", 1)); }
sentence = "the quick brown fox" puts sentence.upcase p sentence.split(" ") puts sentence.include?("quick") puts sentence.sub("quick", "slow")
Ruby calls string methods directly on the string object — upcase vs. to_uppercase, include? vs. contains. Methods ending in ? conventionally return a boolean. Ruby's sub replaces the first match while gsub replaces all of them; no count argument is needed.
Formatting numbers and values
fn main() { let price = 1234.5_f64; println!("{:.2}", price); println!("{:08.2}", price); println!("{:x}", 255_u32); }
price = 1234.5 puts format("%.2f", price) puts format("%08.2f", price) puts format("%x", 255)
Ruby's format (aliased as sprintf) uses C-style format verbs — the same ones you know from printf!-style formatting. The difference is that format returns the formatted string rather than printing it, so it pairs with puts. The terse "%.2f" % price operator form also exists.
Multiline & raw strings
fn main() { let text = "line one line two line three"; println!("{text}"); // Raw string — no escape processing: let path = r"C:\Users\name\file.txt"; println!("{path}"); }
text = "line one line two line three" puts text # Heredoc — strips leading indentation: heredoc = <<~TEXT line one line two TEXT puts heredoc # Single-quoted string — no escape processing: path = 'C:\Users\name\file.txt' puts path
Ruby's heredoc syntax (<<~DELIMITER) strips leading indentation and is idiomatic for multiline strings embedded in code. Single-quoted strings suppress escape sequences — backslashes are literal — serving the same role as Rust's raw string literals (r"...") for paths and patterns.
Numbers
Integer and float division
fn main() { println!("{}", 7 / 2); // 3 (integer division) println!("{}", 7.0 / 2.0); // 3.5 println!("{}", 7_f64 / 2_f64); // 3.5 with suffix println!("{}", 7 % 2); // 1 (remainder) }
puts 7 / 2 # 3 (integer division) puts 7.0 / 2.0 # 3.5 puts 7.fdiv(2) # 3.5 via method puts 7 % 2 # 1
Both languages perform integer division when both operands are integers. Where Rust requires an explicit type suffix or cast to f64, Ruby offers Integer#fdiv for a float result or you can simply write one literal as 7.0. The % remainder operator behaves identically in both.
Arbitrary-precision integers
fn main() { // i128 holds up to 2^127-1; 2^100 fits fine: let result: i128 = 2_i128.pow(100); println!("{result}"); // But 2^200 overflows even i128 — checked_pow catches it: match 2_i128.checked_pow(200) { Some(value) => println!("{value}"), None => println!("overflow: need an arbitrary-precision library"), } }
puts 2 ** 100 puts (2 ** 200).class # Integer — no overflow ever
Rust's integer types are fixed-width: i128 holds 2^100 but overflows at 2^200, and checked_pow returns None instead of silently wrapping. Ruby's Integer promotes to arbitrary precision automatically — 2 ** 200 returns an ordinary Integer with no external library or overflow to guard against.
Numeric literal syntax
fn main() { let million = 1_000_000_i64; let ratio = 3.14_f64; let hex = 0xFF_u8; let binary = 0b1010_u8; println!("{million} {ratio} {hex} {binary}"); }
million = 1_000_000 ratio = 3.14 hex = 0xFF binary = 0b1010 puts "#{million} #{ratio} #{hex} #{binary}"
Both languages support underscores in numeric literals for readability and the 0x (hex) and 0b (binary) prefixes. Ruby does not require type suffixes like _i64 or _f64 — the numeric type is inferred from the literal and can change on reassignment.
Arrays & Hashes
Arrays & Vecs
fn main() { let mut numbers = vec![1, 2, 3]; numbers.push(4); numbers.push(5); println!("{numbers:?}"); println!("{:?}", &numbers[1..3]); println!("{}", numbers.len()); }
numbers = [1, 2, 3] numbers.push(4, 5) p numbers p numbers[1..2] puts numbers.length
Rust's growable sequence is Vec<T>, which must be declared let mut to allow mutation. Ruby's Array is always mutable, heterogeneous by nature, and grows with push (or the append operator <<). Slicing uses a Range like 1..2 — inclusive on both ends — rather than Rust's half-open 1..3.
Iterating arrays
fn main() { let fruits = vec!["apple", "banana", "cherry"]; for (index, fruit) in fruits.iter().enumerate() { println!("{index}: {fruit}"); } }
fruits = ["apple", "banana", "cherry"] fruits.each_with_index do |fruit, index| puts "#{index}: #{fruit}" end
Ruby iterates by sending a block to a collection method — the collection controls the loop, not the caller. Note that each_with_index yields (element, index) in that order, the reverse of Rust's enumerate which yields (index, element). Ruby has no .iter() call; all objects respond to iteration methods directly.
Map, filter, reduce
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|&number| number * 2).collect(); let even_sum: i32 = numbers.iter().filter(|&&number| number % 2 == 0).sum(); println!("{doubled:?} {even_sum}"); }
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |number| number * 2 } even_sum = numbers.select(&:even?).sum p doubled puts even_sum
Ruby's Enumerable module provides map, select, reject, reduce, sum, and dozens more on every collection, without the .iter() and .collect() calls that Rust's iterator pipeline requires. The &:even? shorthand turns a symbol into a block that calls the named method on each element.
Sorting
fn main() { let mut numbers = vec![3, 1, 4, 1, 5, 9, 2]; numbers.sort(); println!("{numbers:?}"); let mut words = vec!["banana", "apple", "cherry"]; words.sort_by_key(|word| word.len()); println!("{words:?}"); }
numbers = [3, 1, 4, 1, 5, 9, 2] p numbers.sort words = ["banana", "apple", "cherry"] p words.sort_by(&:length)
Array#sort returns a new sorted array, leaving the original unchanged — use sort! to sort in place. For a custom key, sort_by takes a block that maps each element to its sort key and computes it only once, making it equivalent to Rust's sort_by_key but terser.
Hashes vs. HashMaps
use std::collections::HashMap; fn main() { let mut ages: HashMap<&str, i32> = HashMap::new(); ages.insert("Alice", 30); ages.insert("Bob", 25); ages.remove("Bob"); println!("{} {}", ages["Alice"], ages.len()); }
ages = { "Alice" => 30, "Bob" => 25 } ages.delete("Bob") puts "#{ages["Alice"]} #{ages.length}"
Ruby's Hash is a built-in literal — write { key => value } or use the symbol shorthand { name: value }. No use statement and no HashMap::new() are needed. Ruby hashes also preserve insertion order when iterated, unlike Rust's HashMap which provides no ordering guarantee.
Missing keys
use std::collections::HashMap; fn main() { let mut inventory: HashMap<&str, i32> = HashMap::new(); inventory.insert("apples", 5); // get returns Option<&V> match inventory.get("bananas") { Some(count) => println!("bananas: {count}"), None => println!("not stocked"), } let count = inventory.get("bananas").copied().unwrap_or(0); println!("{count}"); }
inventory = { "apples" => 5 } count = inventory["bananas"] puts "not stocked" if count.nil? puts inventory.fetch("bananas", 0) # default value
Rust's HashMap::get returns Option<&V>, making the missing-key case explicit in the type system and forcing the caller to handle it. Ruby's [] returns nil for a missing key, which you check with .nil?. The fetch method lets you supply a default or raise KeyError when a key must be present.
Control Flow
Conditionals
fn main() { let score = 85; if score >= 90 { println!("A"); } else if score >= 80 { println!("B"); } else { println!("C"); } }
score = 85 if score >= 90 puts "A" elsif score >= 80 puts "B" else puts "C" end
Ruby drops the parentheses around the condition, closes blocks with end instead of braces, and uses elsif (not else if). It also has a postfix modifier form — puts "B" if score >= 80 — and an unless keyword for negated conditions, neither of which Rust provides.
Loops
fn main() { for index in 0..3 { println!("{index}"); } let mut count = 0; while count < 3 { count += 1; } println!("count: {count}"); }
3.times do |index| puts index end count = 0 while count < 3 count += 1 end puts "count: #{count}"
Ruby's idiomatic style avoids explicit counter loops in favor of sending a block to an object: 3.times, (1..5).each, or array.each. This works because everything — including the integer 3 — is an object that responds to methods. The while loop is available for conditions that do not fit a simple iteration.
Ranges
fn main() { // Half-open range (excludes end): 1..5 for number in 1..5 { print!("{number} "); } println!(); // Inclusive range: 1..=5 for number in 1..=5 { print!("{number} "); } println!(); }
(1...5).each { |number| print "#{number} " } puts (1..5).each { |number| print "#{number} " } puts
Ruby and Rust use opposite range conventions — easy to get wrong when switching between them. Rust's 1..5 is half-open (excludes 5) and 1..=5 is inclusive. Ruby's 1..5 is inclusive (includes 5) and 1...5 is exclusive (excludes 5). The extra dot in Ruby means "exclude the end."
if as an expression
fn main() { let score = 75; let grade = if score >= 60 { "pass" } else { "fail" }; println!("{grade}"); }
score = 75 grade = if score >= 60 then "pass" else "fail" end puts grade # Ternary also exists: status = score >= 60 ? "ok" : "low" puts status
Both languages treat if/else as an expression that evaluates to a value and can be assigned. Rust requires braces around each branch; Ruby uses then (or a newline) after the condition and closes with end. Ruby also supports the C-style ternary condition ? a : b.
break and next
fn main() { let mut count = 0; loop { count += 1; if count == 3 { break; } } for number in 1..=10 { if number % 2 == 0 { continue; } print!("{number} "); } println!(); }
count = 0 loop do count += 1 break if count == 3 end (1..10).each do |number| next if number.even? print "#{number} " end puts
Both use break to exit a loop early. Ruby uses next where Rust uses continue to skip to the next iteration. Ruby's loop do ... end corresponds to Rust's infinite loop {}, and postfix form is idiomatic: break if count == 3 reads like plain English.
Methods
Defining methods
fn greet(name: &str) -> String { format!("Hello, {name}") } fn main() { println!("{}", greet("Ruby")); }
def greet(name) "Hello, #{name}" end puts greet("Ruby")
A Ruby method needs no parameter types, no return type annotation, and usually no return keyword — the value of the last expression evaluated is returned automatically. Ruby 4.0 also supports an endless one-liner: def greet(name) = "Hello, #{name}".
Keyword arguments
// Rust has no keyword arguments; all are positional. fn connect(host: &str, port: u16, secure: bool) -> String { format!("{host}:{port} secure={secure}") } fn main() { println!("{}", connect("localhost", 5432, true)); }
def connect(host:, port: 5432, secure: false) "#{host}:#{port} secure=#{secure}" end puts connect(host: "localhost", secure: true)
Ruby supports named keyword arguments with optional defaults — a feature Rust lacks entirely. A keyword like port: 5432 is optional with a default value, while host: with no default is required. Callers pass keyword arguments by name in any order, eliminating the long positional argument lists that Rust forces.
Variadic arguments
fn sum_numbers(numbers: &[i32]) -> i32 { numbers.iter().sum() } fn main() { println!("{}", sum_numbers(&[1, 2, 3, 4])); }
def sum_numbers(*numbers) numbers.sum end puts sum_numbers(1, 2, 3, 4)
Ruby's splat operator *numbers gathers any number of positional arguments into an array — the idiomatic equivalent of a Rust function that accepts a slice &[T]. At the call site, an existing array can be spread back into arguments with *: sum_numbers(*[1, 2, 3]).
Multiple return values
fn min_max(numbers: &[i32]) -> (i32, i32) { let smallest = *numbers.iter().min().unwrap(); let largest = *numbers.iter().max().unwrap(); (smallest, largest) } fn main() { let (low, high) = min_max(&[3, 1, 4, 1, 5]); println!("{low} {high}"); }
def min_max(numbers) [numbers.min, numbers.max] end low, high = min_max([3, 1, 4, 1, 5]) puts "#{low} #{high}"
Rust returns multiple values as a tuple destructured at the call site. Ruby simulates multiple return values by returning an Array and relying on parallel assignment to unpack it (low, high = ...). The splat form also works: first, *rest = [1, 2, 3] binds rest to [2, 3].
Closures as arguments
fn apply<F: Fn(i32) -> i32>(value: i32, transform: F) -> i32 { transform(value) } fn main() { let result = apply(5, |number| number * 2); println!("{result}"); }
def apply(value) yield value end result = apply(5) { |number| number * 2 } puts result
Rust accepts a closure as a generic function argument bounded by Fn, which must be declared in the function signature. Ruby uses yield — every method can implicitly receive one anonymous block without declaring it as a parameter. The block follows the method call, delimited by { ... } or do ... end.
Blocks & Closures
Closures
fn make_counter() -> impl FnMut() -> i32 { let mut count = 0; move || { count += 1; count } } fn main() { let mut counter = make_counter(); println!("{} {} {}", counter(), counter(), counter()); }
def make_counter count = 0 -> { count += 1 } end counter = make_counter puts "#{counter.call} #{counter.call} #{counter.call}"
Both languages have closures that capture surrounding variables. Rust closures that mutate captured state must use move and be typed as FnMut, and the caller variable must be let mut. A Ruby lambda (->) captures variables by reference automatically, and calling it with .call (or counter.() or counter[]) is always the same regardless of whether state is mutated.
yield — Ruby's signature feature
// In Rust, a callable must be declared explicitly in the signature. fn repeat<F: Fn(i32)>(times: i32, action: F) { for index in 0..times { action(index); } } fn main() { repeat(3, |index| println!("tick {index}")); }
def repeat(times) index = 0 while index < times yield index index += 1 end end repeat(3) do |index| puts "tick #{index}" end
The yield keyword is Ruby's signature feature — a method can pass control to an anonymous block the caller provided without declaring it as a parameter at all. This is how every Ruby iterator works: array.each, 3.times, and hash.select all yield to the caller's block. Rust has no equivalent; closures must always be declared explicitly in the function signature.
Optional block (block_given?)
fn greet_with<F: Fn(&str)>(name: &str, formatter: Option<F>) { match formatter { Some(transform) => transform(name), None => println!("Hello, {name}!"), } } fn main() { greet_with("Alice", Some(|name: &str| println!("Hey, {name}!"))); let no_op: Option<fn(&str)> = None; greet_with("Bob", no_op); }
def greet_with(name) if block_given? yield name else puts "Hello, #{name}!" end end greet_with("Alice") { |name| puts "Hey, #{name}!" } greet_with("Bob")
Ruby's block_given? tests whether the caller supplied a block, allowing a method to have both a default behavior and a block-customizable one. Rust achieves the same optionality with Option<F>, which must appear in the function signature explicitly. Ruby's approach is more flexible because the method does not need to know about the block at definition time.
Stored callables & &:method
fn main() { let double = |number: i32| number * 2; let numbers = vec![1, 2, 3]; let doubled: Vec<i32> = numbers.iter().map(|&number| double(number)).collect(); println!("{doubled:?}"); }
double = ->(number) { number * 2 } numbers = [1, 2, 3] p numbers.map(&double) # Turn a method name into a block: p numbers.map(&:to_s)
A stored anonymous function in Ruby is a Proc or lambda. The & operator converts one into the block that a method expects, so map(&double) applies it to each element. The symbol form &:to_s is shorthand for "call to_s on each item" — one of the most widely used Ruby idioms.
Iterator / Enumerable chaining
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let result: i32 = numbers.iter() .filter(|&&number| number % 2 == 0) .map(|&number| number * number) .take(3) .sum(); println!("{result}"); }
result = (1..10).to_a .select(&:even?) .map { |number| number * number } .first(3) .sum puts result
Ruby's Enumerable provides the same lazy-pipeline feel as Rust's iterator adapters: select is filter, map is map, first(n) is take(n), and sum closes the chain. The Ruby version requires no .iter() to begin or .collect() to materialize — the result type is inferred automatically.
Classes & Objects
Structs vs. classes
struct Point { x: i32, y: i32, } fn main() { let point = Point { x: 1, y: 2 }; println!("{} {}", point.x, point.y); }
class Point attr_accessor :x, :y def initialize(x, y) @x = x @y = y end end point = Point.new(1, 2) puts "#{point.x} #{point.y}"
Rust separates data (struct) from behavior (impl block). Ruby combines both in a class body. State lives in instance variables prefixed with @, which are private by default; attr_accessor generates getter and setter methods. The constructor is always named initialize and is invoked by ClassName.new.
impl vs. class body
struct Rectangle { width: f64, height: f64, } impl Rectangle { fn new(width: f64, height: f64) -> Self { Rectangle { width, height } } fn area(&self) -> f64 { self.width * self.height } } fn main() { println!("{}", Rectangle::new(3.0, 4.0).area()); }
class Rectangle def initialize(width, height) @width = width @height = height end def area @width * @height end end puts Rectangle.new(3.0, 4.0).area
In Rust, impl Rectangle attaches methods to the struct in a separate block, and instance methods take &self as an explicit first parameter. In Ruby, methods are defined inside the class body and implicitly operate on self without declaring it — the receiver is always the current object.
Inheritance
// Rust has no inheritance — shared behavior comes from traits. trait Speak { fn speak(&self) -> String; } struct Dog { name: String } impl Speak for Dog { fn speak(&self) -> String { format!("{} says: woof!", self.name) } } fn announce(speaker: &dyn Speak) { println!("{}", speaker.speak()); } fn main() { announce(&Dog { name: "Rex".to_string() }); }
class Animal def initialize(name) @name = name end def speak "#{@name} makes a sound" end end class Dog < Animal def speak super + " (woof!)" end end puts Dog.new("Rex").speak
Rust has no classical inheritance — shared behavior is composed through traits, not parent classes. Ruby has single inheritance with class Dog < Animal; a subclass can override a method and reach the parent's version with super. This "is-a" relationship gives Ruby a simpler model for behavior reuse compared to Rust's composition-only approach.
Open classes
// In Rust, you cannot add methods to types from another crate. // Extension traits work within your own crate only. trait Shout { fn shout(&self) -> String; } impl Shout for str { fn shout(&self) -> String { format!("{}!", self.to_uppercase()) } } fn main() { println!("{}", "hello".shout()); }
class String def shout upcase + "!" end end puts "hello".shout
Ruby classes are open — you can reopen any class, including built-in ones like String, Integer, or Array, and add or redefine methods at runtime. Rust extension traits provide a similar capability within your own crate, but the orphan rule prevents adding methods to external types. Ruby's openness is more powerful but carries the responsibility not to surprise other code.
Comparable / Ord
use std::cmp::Ordering; struct Person { name: String, age: u32 } impl Ord for Person { fn cmp(&self, other: &Self) -> Ordering { self.age.cmp(&other.age) } } impl PartialOrd for Person { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) } } impl PartialEq for Person { fn eq(&self, other: &Self) -> bool { self.age == other.age } } impl Eq for Person {} fn main() { let mut people = vec![ Person { name: "Alice".to_string(), age: 30 }, Person { name: "Bob".to_string(), age: 25 }, ]; people.sort(); for person in &people { println!("{} {}", person.name, person.age); } }
class Person include Comparable attr_reader :name, :age def initialize(name, age) @name = name @age = age end def <=>(other) age <=> other.age end end people = [Person.new("Alice", 30), Person.new("Bob", 25)] puts people.sort.map(&:name).inspect
Ruby's Comparable mixin — activated with include Comparable and a single <=> ("spaceship") operator — grants <, >, <=, >=, sort, min, max, between?, and clamp for free. Rust requires implementing Ord, PartialOrd, PartialEq, and Eq separately — far more boilerplate for the same result.
Modules & Mixins
Modules as namespaces
mod geometry { pub struct Circle { pub radius: f64 } impl Circle { pub fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } } } fn main() { let circle = geometry::Circle { radius: 3.0 }; println!("{:.2}", circle.area()); }
module Geometry class Circle def initialize(radius) @radius = radius end def area Math::PI * @radius ** 2 end end end circle = Geometry::Circle.new(3.0) puts circle.area.round(2)
Both mod (Rust) and module (Ruby) serve as namespaces accessed with ::. The key difference is that Ruby modules serve a dual role: in addition to namespacing, they can be included as mixins that inject method implementations into a class — a role Rust separates into traits.
Traits vs. mixins
trait Greetable { fn name(&self) -> &str; fn hello(&self) -> String { format!("Hello, I'm {}!", self.name()) } } struct User { username: String } impl Greetable for User { fn name(&self) -> &str { &self.username } } fn main() { let user = User { username: "Alice".to_string() }; println!("{}", user.hello()); }
module Greetable def hello "Hello, I'm #{name}!" end end class User include Greetable attr_reader :name def initialize(name) @name = name end end puts User.new("Alice").hello
A Ruby module included as a mixin injects its method implementations directly into the class — calling include Greetable means User objects respond to hello with no interface declaration or type annotation. Rust traits with default method implementations are the closest equivalent, but the type must still explicitly impl the trait.
Enumerable — the power mixin
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let above_two: Vec<&i32> = numbers.iter().filter(|&&n| n > 2).collect(); let scaled: Vec<i32> = above_two.iter().map(|&&n| n * 10).collect(); println!("{scaled:?}"); println!("min={:?} max={:?}", numbers.iter().min(), numbers.iter().max()); }
numbers = [1, 2, 3, 4, 5] scaled = numbers.select { |number| number > 2 }.map { |number| number * 10 } p scaled puts "min=#{numbers.min} max=#{numbers.max}"
Enumerable is the most powerful Ruby mixin — include Enumerable and define one method (each) to gain more than 50 methods: map, select, reject, min, max, sum, group_by, tally, flat_map, and many more. Rust achieves the same through the Iterator trait's default methods, but you must always enter the pipeline explicitly with .iter().
Operator overloading
use std::ops::Add; #[derive(Debug)] struct Vector2D { x: f64, y: f64 } impl Add for Vector2D { type Output = Vector2D; fn add(self, other: Vector2D) -> Vector2D { Vector2D { x: self.x + other.x, y: self.y + other.y } } } fn main() { let sum = Vector2D { x: 1.0, y: 2.0 } + Vector2D { x: 3.0, y: 4.0 }; println!("{sum:?}"); }
class Vector2D attr_reader :x, :y def initialize(x, y) @x = x @y = y end def +(other) Vector2D.new(@x + other.x, @y + other.y) end def inspect "Vector2D(#{@x}, #{@y})" end end puts (Vector2D.new(1.0, 2.0) + Vector2D.new(3.0, 4.0)).inspect
Both languages support operator overloading. Ruby defines operators as ordinary methods — def +, def ==, def [] — and the interpreter calls them when the operator is used. Rust implements the corresponding standard trait (Add, PartialEq, Index) — a more structured approach that leverages the type system.
Enums & Symbols
Simple enums vs. symbols
enum Direction { North, South, East, West } fn describe(direction: Direction) -> &'static str { match direction { Direction::North => "up", Direction::South => "down", Direction::East => "right", Direction::West => "left", } } fn main() { println!("{}", describe(Direction::North)); println!("{}", describe(Direction::West)); }
def describe(direction) case direction when :north then "up" when :south then "down" when :east then "right" when :west then "left" end end puts describe(:north) puts describe(:west)
Rust enums are sum types enforced by the compiler — every match must handle every variant or it will not compile. Ruby uses symbols (:north) as lightweight, immutable identifiers in place of simple enums, combined with case/when for dispatch. The switch is not exhaustiveness-checked, so an unmatched value silently returns nil.
Enums with data
#[derive(Debug)] enum Shape { Circle(f64), Rectangle(f64, f64), } fn area(shape: &Shape) -> f64 { match shape { Shape::Circle(radius) => std::f64::consts::PI * radius * radius, Shape::Rectangle(width, height) => width * height, } } fn main() { let shapes = vec![Shape::Circle(3.0), Shape::Rectangle(4.0, 5.0)]; for shape in &shapes { println!("{shape:?} → {:.2}", area(shape)); } }
circle = { type: :circle, radius: 3.0 } rectangle = { type: :rectangle, width: 4.0, height: 5.0 } def area(shape) case shape in { type: :circle, radius: } then Math::PI * radius ** 2 in { type: :rectangle, width:, height: } then width * height end end [circle, rectangle].each { |shape| puts area(shape).round(2) }
Rust enums can carry data — each variant is essentially an anonymous struct. Ruby has no built-in equivalent, but hash pattern matching (case/in) achieves a similar dispatch: the pattern destructures the hash and binds its keys as local variables. The Ruby approach is flexible but provides no compile-time exhaustiveness guarantee.
Symbols
// Rust has no Symbol type; &'static str is idiomatic for string constants. fn main() { let direction: &str = "north"; let status: &str = "active"; println!("{direction} {status}"); println!("{}", direction == "north"); // value comparison }
direction = :north status = :active puts "#{direction} #{status}" puts :north == :north # true — always the same object puts :north.equal?(:north) # true — identity, not just value
A Ruby Symbol (:name) is an immutable, interned identifier — :north is always the exact same object in memory regardless of where it appears, making identity comparison (equal?) and equality (==) identical. Symbols are the idiomatic key type for hashes acting as records, and the &:method shorthand depends on them. Rust has no equivalent.
Error Handling
Result vs. exceptions
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> { if denominator == 0.0 { return Err("division by zero".to_string()); } Ok(numerator / denominator) } fn main() { match divide(10.0, 0.0) { Ok(result) => println!("{result}"), Err(error) => println!("error: {error}"), } }
def divide(numerator, denominator) raise ArgumentError, "division by zero" if denominator.zero? numerator / denominator end begin puts divide(10.0, 0.0) rescue ArgumentError => error puts "error: #{error.message}" end
This is the deepest philosophical split between the two languages. Rust returns errors as ordinary Result values that the caller must inspect before using the value. Ruby raises exceptions that unwind the call stack until a rescue catches them — the happy path is never interrupted by error checks, and failures propagate automatically until something handles them.
Option vs. nil
fn find_user(name: &str) -> Option<String> { if name == "Alice" { Some(format!("User: {name}")) } else { None } } fn main() { match find_user("Alice") { Some(user) => println!("{user}"), None => println!("not found"), } println!("{}", find_user("Bob").unwrap_or("not found".to_string())); }
def find_user(name) return "User: #{name}" if name == "Alice" nil end user = find_user("Alice") puts user.nil? ? "not found" : user puts find_user("Bob") || "not found"
Rust's Option<T> makes the presence or absence of a value part of the type — the compiler forces you to handle None before accessing the inner value. Ruby uses nil as the universal "no value", checked with .nil? or the || short-circuit. The Ruby approach is simpler but provides no compile-time guarantee that the missing case has been handled.
The ? operator vs. rescue propagation
use std::num::ParseIntError; fn parse_and_double(input: &str) -> Result<i32, ParseIntError> { let number = input.trim().parse::<i32>()?; Ok(number * 2) } fn main() { println!("{:?}", parse_and_double("21")); println!("{:?}", parse_and_double("oops")); }
def parse_and_double(input) Integer(input.strip) * 2 rescue ArgumentError => error raise "parse error: #{error.message}" end puts parse_and_double("21") begin parse_and_double("oops") rescue => error puts error.message end
Rust's ? operator is early-return sugar for Result — if the expression is Err, the function returns that error immediately. Ruby achieves the same propagation through exceptions: a failure in Integer(...) raises ArgumentError, which travels up the call stack automatically without any explicit check at the call site.
RAII vs. ensure
fn main() { // RAII: cleanup is automatic when the scope ends. { let resource = String::from("file handle"); println!("using: {resource}"); } // resource dropped here — no explicit cleanup needed println!("resource released"); }
def process puts "working" ensure puts "cleanup runs — even if an exception was raised" end process
Rust's memory and resource cleanup happens deterministically through RAII — when a value's scope ends, its Drop implementation runs automatically. Ruby's ensure clause is explicit: it runs when a method or begin block exits, whether normally or via an exception, and is the place to close files, release locks, or finalize resources.
Custom error types
use std::fmt; #[derive(Debug)] struct NotFoundError { key: String } impl fmt::Display for NotFoundError { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "not found: {}", self.key) } } fn lookup(key: &str) -> Result<i32, NotFoundError> { Err(NotFoundError { key: key.to_string() }) } fn main() { match lookup("user:42") { Ok(value) => println!("{value}"), Err(error) => println!("{error}"), } }
class NotFoundError < StandardError def initialize(key) super("not found: #{key}") end end begin raise NotFoundError.new("user:42") rescue NotFoundError => error puts error.message end
A Rust custom error implements Display (for human-readable messages) and optionally Error and Debug. A Ruby custom exception is a class that inherits from StandardError. The rescue clause matches by class — rescue NotFoundError catches only that class and its subclasses — making it easy to build an exception hierarchy.
Pattern Matching
Basic match / case
fn main() { let number = 3; let description = match number { 1 => "one", 2 | 3 => "two or three", 4..=9 => "four to nine", _ => "something else", }; println!("{description}"); }
number = 3 description = case number when 1 then "one" when 2, 3 then "two or three" when 4..9 then "four to nine" else "something else" end puts description
Ruby's case/when maps closely to Rust's match. Both support multiple values in one arm (comma in Ruby, | in Rust) and range patterns. The key difference is exhaustiveness: Rust requires a _ wildcard to compile if not all cases are covered, while Ruby's case silently returns nil for an unmatched value.
Structural destructuring
fn main() { let point = (0, 5); match point { (0, 0) => println!("origin"), (0, y) => println!("on y-axis at {y}"), (x, 0) => println!("on x-axis at {x}"), (x, y) => println!("at ({x}, {y})"), } }
point = [0, 5] case point in [0, 0] puts "origin" in [0, y] puts "on y-axis at #{y}" in [x, 0] puts "on x-axis at #{x}" in [x, y] puts "at (#{x}, #{y})" end
Ruby's case/in syntax (stable since Ruby 3.0) provides structural pattern matching that mirrors Rust's match very closely. Both support destructuring and variable binding in the same clause. Ruby uses square brackets for arrays where Rust uses parentheses for tuples — the semantics are otherwise identical.
Hash / struct patterns
use std::collections::HashMap; fn main() { let user = HashMap::from([("role", "admin"), ("name", "Alice")]); match (user.get("role").copied(), user.get("name").copied()) { (Some("admin"), Some(name)) => println!("admin: {name}"), _ => println!("guest"), } }
user = { role: "admin", name: "Alice" } case user in { role: "admin", name: } puts "admin: #{name}" else puts "guest" end
Ruby's hash pattern is one of the most concise forms in the language. A pattern like { role: "admin", name: } both checks that :role equals "admin" and binds :name's value to the local variable name in one clause. Rust must manually extract and test map values since it has no native hash pattern syntax.
Pattern guards
fn main() { let number = 15; let description = match number { n if n < 0 => "negative", 0 => "zero", n if n % 2 == 0 => "positive even", _ => "positive odd", }; println!("{description}"); }
number = 15 description = case number in (..0) then "negative" in 0 then "zero" in n if n.even? then "positive even" else "positive odd" end puts description
Both Rust and Ruby pattern matching support guard conditions — a filter applied after the structural pattern succeeds. Rust uses if condition inline; Ruby's case/in also uses if. A guard is checked only if the structural pattern already matched, so the behavior is identical. Ruby also supports range patterns like (..0) for "up to and including 0."
Concurrency
Threads
use std::thread; fn main() { let handle = thread::spawn(|| { println!("hello from thread"); }); handle.join().unwrap(); println!("main done"); }
thread = Thread.new do puts "hello from thread" end thread.join puts "main done"
Both thread::spawn (Rust) and Thread.new (Ruby) launch threads, and join waits for them. The critical difference: Rust threads run truly in parallel — the borrow checker prevents data races at compile time. Ruby's standard interpreter (CRuby) has a global interpreter lock (GIL) that prevents pure-Ruby threads from running on multiple CPU cores simultaneously; they shine for concurrent I/O, not CPU-bound parallelism.
Shared mutable state
use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0_i32)); let handles: Vec<_> = (0..5).map(|_| { let counter = Arc::clone(&counter); thread::spawn(move || { let mut guard = counter.lock().unwrap(); *guard += 1; }) }).collect(); for handle in handles { handle.join().unwrap(); } println!("{}", counter.lock().unwrap()); }
mutex = Mutex.new counter = 0 threads = 5.times.map do Thread.new { mutex.synchronize { counter += 1 } } end threads.each(&:join) puts counter
Rust requires Arc<Mutex<T>> to safely share mutable state across threads — the borrow checker enforces this at compile time and the pattern is verbose but correct by construction. Ruby's Mutex#synchronize takes a block and releases the lock automatically when the block exits (even on an exception), removing the need to manually pair lock and unlock.
Collecting thread results
use std::thread; fn main() { let handles: Vec<_> = (0..4) .map(|index| thread::spawn(move || index * index)) .collect(); let results: Vec<i32> = handles.into_iter() .map(|handle| handle.join().unwrap()) .collect(); println!("{results:?}"); }
threads = (0...4).map do |index| Thread.new(index) { |position| position * position } end results = threads.map(&:value) p results
Thread#value blocks until the thread finishes and returns the value of its block — so threads.map(&:value) collects all results in order with no explicit synchronization. Rust's handle.join().unwrap() does the same per handle; iterating with map achieves the same collection pattern but requires more ceremony.