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.