PONY λ M2 Modula-2

PHP.CodeCompared.To/Rust

An interactive executable cheatsheet comparing PHP and Rust

PHP 8.3.11 (Judge0) or PHP 8.5.5 (WASM) Rust 1.95
Output & Running
Hello, World
<?php echo "Hello, World!\n";
println!("Hello, World!");
Output uses println! — the trailing ! marks it a macro, not a function, because it does compile-time checking of the format string. It appends a newline for you, so you do not write \n. There is no <?php tag and no $ on variables.
Interpreted vs Compiled
<?php // PHP is interpreted — run the source directly: // php script.php $name = "World"; echo "Hello, {$name}!\n";
// Rust compiles to a native binary with no runtime: // rustc script.rs && ./script // Real projects use Cargo, the build tool + package manager: // cargo run let name = "World"; println!("Hello, {name}!");
Rust is compiled ahead of time to a self-contained native binary with no interpreter or garbage collector to ship — like C, but memory-safe. Cargo is the standard tool (PHP's Composer plus a build system): cargo new, cargo build, cargo run, and crates from crates.io.
Formatted Output
<?php $name = "Alice"; $age = 30; printf("%s is %d\n", $name, $age); echo "{$name} is {$age}\n";
let name = "Alice"; let age = 30; println!("{name} is {age}"); // inline captures println!("{} is {}", name, age); // positional println!("{:?}", (name, age)); // Debug formatting
The format string captures variables inline ({name}), much like PHP's "{$name}", or takes positional arguments with empty {}. The {:?} form is Debug output — a developer-facing representation of almost any value, akin to var_export.
Variables & Mutability
let & Immutability
<?php $count = 42; $count = 43; // PHP variables are always mutable echo $count, "\n";
let count = 42; // count = 43; // compile error — bindings are immutable by default let mut total = 42; total = 43; // OK — 'mut' makes it mutable println!("{total}");
The biggest habit change: a let binding is immutable by default. To reassign, you must opt in with let mut. This is the opposite of PHP, where every variable is freely mutable, and it pushes you toward code where what can change is explicit.
Scalar Types
<?php $integer = 7; // int $float = 3.5; // float $flag = true; // bool $letter = "A"; // string (no char type) echo gettype($integer), " ", gettype($float), "\n";
let integer: i32 = 7; // 32-bit signed integer let float: f64 = 3.5; // 64-bit float let flag: bool = true; let letter: char = 'A'; // a single Unicode scalar println!("{integer} {float} {flag} {letter}");
Rust has a family of sized numeric types — i32, i64, u8, usize, f32, f64 — instead of PHP's single int and float. It also has a distinct char type for one Unicode scalar (single quotes), separate from strings; PHP has no character type.
Shadowing
<?php $value = "42"; $value = (int) $value; // reuse the name, new value (still mutable var) echo $value + 1, "\n"; // 43
let value = "42"; let value: i32 = value.parse().unwrap(); // shadow: new binding, new type println!("{}", value + 1); // 43
Rust lets you shadow a name by declaring let again, even changing the type (here &str to i32). Unlike PHP's reassignment, each let creates a fresh immutable binding, which is the idiomatic way to transform a value through stages without a mutable variable.
Strings
String vs &str
<?php // PHP has one string type: $greeting = "Hello"; $greeting .= ", World"; // mutate in place echo $greeting, "\n";
let literal: &str = "Hello"; // a borrowed string slice let mut owned: String = String::from(literal); owned.push_str(", World"); // grow the owned, heap String println!("{owned}");
Rust splits strings into two types: &str, a borrowed view (often a literal), and String, a growable, heap-owned buffer. You build and mutate a String; you pass around &str to read. PHP's single mutable string covers both roles, so this distinction is new and central.
Common Methods
<?php $text = " Hello, World "; echo strtoupper(trim($text)), "\n"; // HELLO, WORLD echo str_replace("World", "Rust", "Hello World"), "\n"; echo str_contains("Hello", "ell") ? "yes\n" : "no\n";
let text = " Hello, World "; println!("{}", text.trim().to_uppercase()); // HELLO, WORLD println!("{}", "Hello World".replace("World", "Rust")); println!("{}", "Hello".contains("ell")); // true
String slices have chainable methods (text.trim().to_uppercase()) replacing the free functions strtoupper(trim($text)). Note these return new String values rather than mutating in place — Rust's immutability-by-default extends to strings.
Building Strings
<?php $name = "Alice"; $age = 30; $line = sprintf("%s (%d)", $name, $age); echo $line, "\n"; // Alice (30)
let name = "Alice"; let age = 30; let line = format!("{name} ({age})"); // returns a String println!("{line}"); // Alice (30)
The format! macro is println!'s sibling that returns a String instead of printing — the counterpart to PHP's sprintf, but with the same inline-capture and placeholder syntax as println!.
Arrays vs Vec & HashMap
One Array vs Several Types
<?php // PHP's single array is both list and map: $list = [1, 2, 3]; $map = ["name" => "Alice", "age" => "30"]; echo $list[0], " ", $map["name"], "\n";
use std::collections::HashMap; let list: Vec<i32> = vec![1, 2, 3]; // growable vector let mut map: HashMap<&str, &str> = HashMap::new(); map.insert("name", "Alice"); println!("{} {}", list[0], map["name"]);
PHP's one array type splits into a Vec<T> (a growable, homogeneously typed list) and a HashMap<K, V> for key-value data. Both are generic over their element types, fixed at compile time, so a Vec<i32> can never hold a string. There are also fixed-size arrays [i32; 3].
Vec Operations
<?php $numbers = [3, 1, 2]; $numbers[] = 4; // append sort($numbers); echo count($numbers), "\n"; // 4 echo in_array(2, $numbers) ? "yes\n" : "no\n";
let mut numbers = vec![3, 1, 2]; numbers.push(4); numbers.sort(); println!("{}", numbers.len()); // 4 println!("{}", numbers.contains(&2)); // true
A Vec exposes methods — push, sort, len, contains — replacing array_push, sort, count, and in_array. Note contains(&2) takes a reference, and the vector must be mut to push or sort.
Map, Filter & Collect
<?php $numbers = [1, 2, 3, 4, 5]; $result = array_map( fn($n) => $n * 2, array_filter($numbers, fn($n) => $n % 2 === 1) ); print_r(array_values($result)); // 2, 6, 10
let numbers = vec![1, 2, 3, 4, 5]; let result: Vec<i32> = numbers .iter() .filter(|&n| n % 2 == 1) .map(|n| n * 2) .collect(); println!("{:?}", result); // [2, 6, 10]
Rust's iterator adapters filter and map chain left-to-right and are lazy — nothing runs until collect() consumes them into a concrete Vec. This is more readable than PHP's inside-out array_map(array_filter(...)), and the closures capture variables like PHP arrow functions.
Iterating a Map
<?php $scores = ["alice" => 90, "bob" => 85]; foreach ($scores as $name => $score) { echo "{$name}: {$score}\n"; }
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert("alice", 90); scores.insert("bob", 85); let mut total = 0; for (name, score) in &scores { total += score; let _ = name; // (order is not guaranteed) } println!("total: {total}"); // 175
Iterating a map with for (name, score) in &scores destructures each entry, like foreach ... as $name => $score. The & borrows the map so you can keep using it afterward. Unlike a PHP array, a HashMap has no guaranteed order, so this example sums rather than printing per-entry.
Control Flow
if as an Expression
<?php $score = 85; $grade = $score >= 90 ? "A" : ($score >= 80 ? "B" : "C"); echo $grade, "\n"; // B
let score = 85; let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" }; println!("{grade}"); // B
In Rust if is an expression that yields a value, so it replaces the ternary operator (which Rust does not have): the last expression in each branch becomes the result. Conditions take no parentheses, and every branch must produce the same type.
Loops & Ranges
<?php for ($index = 0; $index < 3; $index++) { echo $index, " "; } echo "\n"; foreach (["a", "b", "c"] as $letter) { echo $letter, " "; } echo "\n";
for index in 0..3 { // 0..3 is a half-open range print!("{index} "); } println!(); for letter in ["a", "b", "c"] { print!("{letter} "); } println!();
Rust has no C-style three-part for; you iterate over a range (0..3, which stops before 3) or any iterable. The foreach-over-values form maps to for letter in [...]. Rust also has loop { } for an infinite loop and while.
match vs match
<?php $status = 404; $message = match (true) { $status < 300 => "ok", $status < 400 => "redirect", $status < 500 => "client error", default => "server error", }; echo $message, "\n"; // client error
let status = 404; let message = match status { s if s < 300 => "ok", s if s < 400 => "redirect", s if s < 500 => "client error", _ => "server error", }; println!("{message}"); // client error
Both languages have a value-returning match, and PHP's was partly inspired by Rust's. Rust's is more powerful: arms are patterns, if guards refine them, the wildcard is _ instead of default, and the compiler enforces that the match is exhaustive.
Ownership & Borrowing
Ownership & Move
<?php // PHP just reference-counts and copies arrays as values; you never // think about who "owns" a value: $original = [1, 2, 3]; $copy = $original; // independent copy (arrays are value types) $copy[] = 4; echo count($original), "\n"; // 3
let original = String::from("hello"); let moved = original; // ownership MOVES to 'moved' // println!("{original}"); // compile error — original was moved println!("{moved}"); // hello let cloned = moved.clone(); // explicit deep copy println!("{moved} {cloned}");
Rust's defining idea: every value has a single owner, and assigning a heap value moves ownership, invalidating the old binding. This is how Rust frees memory deterministically with no garbage collector. To keep both, you clone() explicitly. PHP's reference-counted runtime hides all of this from you.
Borrowing & References
<?php function describe(array $items): string { return count($items) . " items"; } $list = [1, 2, 3]; echo describe($list), "\n"; // 3 items echo count($list), "\n"; // 3 — still usable
fn describe(items: &Vec<i32>) -> String { format!("{} items", items.len()) // borrows, does not take ownership } let list = vec![1, 2, 3]; println!("{}", describe(&list)); // 3 items println!("{}", list.len()); // 3 — still usable
To use a value without taking ownership, you borrow it with & — a reference. The function reads &Vec<i32> and the caller keeps the original. The borrow checker enforces the rules (many readers OR one writer at a time), preventing data races at compile time — guarantees PHP's value-copy model never needs to make.
Mutable References
<?php function appendOne(array &$items): void { // PHP: pass by reference with & $items[] = 1; } $list = []; appendOne($list); appendOne($list); echo count($list), "\n"; // 2
fn append_one(items: &mut Vec<i32>) { items.push(1); } let mut list = Vec::new(); append_one(&mut list); append_one(&mut list); println!("{}", list.len()); // 2
A &mut reference lets a function mutate the caller's value — the parallel of PHP's &$items by-reference parameter. The difference is that Rust permits only one mutable borrow at a time (and none while shared borrows exist), which is what makes the no-data-race guarantee possible.
Functions & Closures
Defining Functions
<?php function add(int $first, int $second): int { return $first + $second; } echo add(2, 3), "\n"; // 5
fn add(first: i32, second: i32) -> i32 { first + second // last expression is the return value — no 'return' } println!("{}", add(2, 3)); // 5
Functions use fn, with parameter types after each name and the return type after a -> arrow. The killer convenience: the final expression is the return value — drop the semicolon and omit return. An explicit return still works for early exits.
Closures
<?php $multiplier = 3; $scale = fn($value) => $value * $multiplier; // auto-captures echo $scale(5), "\n"; // 15
let multiplier = 3; let scale = |value| value * multiplier; // captures automatically println!("{}", scale(5)); // 15
A Rust closure uses pipe-delimited parameters |value| ... and captures surrounding variables automatically, like PHP's arrow function fn() =>. The compiler infers how it captures (by reference, or by move with the move keyword) based on what the body does.
Returning Tuples
<?php function minMax(array $numbers): array { return [min($numbers), max($numbers)]; } [$low, $high] = minMax([3, 7, 1, 9]); echo "{$low} {$high}\n"; // 1 9
fn min_max(numbers: &[i32]) -> (i32, i32) { let low = *numbers.iter().min().unwrap(); let high = *numbers.iter().max().unwrap(); (low, high) } let (low, high) = min_max(&[3, 7, 1, 9]); println!("{low} {high}"); // 1 9
Rust returns multiple values as a typed tuple (i32, i32), which the caller destructures with let (low, high) — much like PHP's "return an array, then destructure", but each position has its own static type. The &[i32] parameter is a slice, a borrowed view over any array or Vec.
Structs & Methods
Structs vs Classes
<?php class Point { public function __construct(public int $x, public int $y) {} public function distance(): float { return sqrt($this->x ** 2 + $this->y ** 2); } } $point = new Point(3, 4); echo $point->distance(), "\n"; // 5
struct Point { x: i32, y: i32, } impl Point { fn distance(&self) -> f64 { ((self.x.pow(2) + self.y.pow(2)) as f64).sqrt() } } let point = Point { x: 3, y: 4 }; println!("{}", point.distance()); // 5
Rust separates data (the struct) from behavior (an impl block of methods). Methods take &self explicitly — the parallel of $this — and you construct with Point { x: 3, y: 4 }, no new keyword. There is no class; this split is the Rust way.
Constructors (Associated Functions)
<?php class Circle { public function __construct(public float $radius) {} public static function unit(): self { return new self(1.0); } } echo Circle::unit()->radius, "\n"; // 1
struct Circle { radius: f64, } impl Circle { fn new(radius: f64) -> Self { Circle { radius } } fn unit() -> Self { Circle::new(1.0) } } println!("{}", Circle::unit().radius); // 1
Rust has no built-in constructor; the convention is an associated function named new (no self parameter), called as Circle::new(...) — the rough equivalent of a PHP static factory. Self is shorthand for the type being implemented, like PHP's self.
Derived Behavior
<?php // PHP gives objects value-comparison and var_export for free: class Point { public function __construct(public int $x, public int $y) {} } $a = new Point(1, 2); $b = new Point(1, 2); echo $a == $b ? "equal\n" : "different\n"; // equal var_dump($a);
#[derive(Debug, PartialEq, Clone)] struct Point { x: i32, y: i32, } let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; println!("{}", a == b); // true (from PartialEq) println!("{:?}", a); // Point { x: 1, y: 2 } (from Debug)
Rust generates common behavior with a #[derive(...)] attribute: Debug enables {:?} printing, PartialEq enables ==, and Clone enables .clone(). PHP grants object value-comparison and var_dump automatically; Rust makes you opt in, which keeps the costs explicit.
Enums & Pattern Matching
Enums
<?php enum Direction: string { case North = "north"; case South = "south"; public function opposite(): self { return match ($this) { Direction::North => Direction::South, Direction::South => Direction::North, }; } } echo Direction::North->opposite()->value, "\n"; // south
#[derive(Debug)] enum Direction { North, South, } impl Direction { fn opposite(&self) -> Direction { match self { Direction::North => Direction::South, Direction::South => Direction::North, } } } println!("{:?}", Direction::North.opposite()); // South
Both languages have rich enums with methods. The syntax rhymes: variants are listed, and methods live in an impl block matching on self. Rust enums go further still — each variant can carry its own typed data, which makes them the foundation of Option and Result below.
Option Instead of null
<?php function findUser(int $id): ?string { return $id === 1 ? "Alice" : null; } $name = findUser(2); echo $name ?? "not found", "\n"; // not found
fn find_user(id: i32) -> Option<String> { if id == 1 { Some(String::from("Alice")) } else { None } } let name = find_user(2); println!("{}", name.unwrap_or(String::from("not found")));
Rust has no null. A value that might be absent is an Option<T> — either Some(value) or None — and the compiler forces you to handle the None case before using the value. unwrap_or supplies a default, the parallel of PHP's ??, but you can never accidentally dereference a null.
Destructuring with if let
<?php $data = ["name" => "Alice"]; if (isset($data["name"])) { $name = $data["name"]; echo "Hello, {$name}\n"; } else { echo "No name\n"; }
let found: Option<&str> = Some("Alice"); if let Some(name) = found { println!("Hello, {name}"); // binds name only if Some } else { println!("No name"); }
The if let construct matches one pattern and binds its contents in a single line — here it unwraps Some(name) only when the Option is present. It is the concise alternative to a full match when you care about just one case, with no PHP equivalent beyond isset-and-assign.
Traits vs Interfaces
Traits as Interfaces
<?php interface Shape { public function area(): float; } class Square implements Shape { public function __construct(private float $side) {} public function area(): float { return $this->side ** 2; } } echo (new Square(3))->area(), "\n"; // 9
trait Shape { fn area(&self) -> f64; } struct Square { side: f64, } impl Shape for Square { fn area(&self) -> f64 { self.side * self.side } } println!("{}", Square { side: 3.0 }.area()); // 9
A trait is Rust's interface: it declares method signatures, and a type provides them with impl Trait for Type — separating the contract from the data. Unlike PHP interfaces, traits can supply default method bodies and can be implemented for types you do not own, which is far more flexible.
Default Methods & Shared Behavior
<?php // PHP shares default behavior with traits (a different meaning of "trait"): trait Greetable { public function greet(): string { return "Hello, " . $this->name(); } abstract public function name(): string; } class Person { use Greetable; public function __construct(private string $name) {} public function name(): string { return $this->name; } } echo (new Person("Alice"))->greet(), "\n";
trait Greetable { fn name(&self) -> String; fn greet(&self) -> String { // default implementation format!("Hello, {}", self.name()) } } struct Person { name: String, } impl Greetable for Person { fn name(&self) -> String { self.name.clone() } } println!("{}", Person { name: String::from("Alice") }.greet());
Confusingly, PHP also has a feature called "traits", but it is a code-reuse mixin. Rust's trait is closer to an interface with optional default methods: greet here has a body that any implementor inherits for free, while name must be provided — combining PHP's interface and trait concepts into one.
Generics
Generic Functions
<?php // PHP has no real generics; you accept mixed and document with docblocks: /** @template T @param T[] $items @return T */ function firstItem(array $items): mixed { return $items[0]; } echo firstItem([10, 20, 30]), "\n"; // 10
fn first_item<T: Copy>(items: &[T]) -> T { items[0] } println!("{}", first_item(&[10, 20, 30])); // 10 println!("{}", first_item(&['a', 'b'])); // a
Generics are a real, compile-time feature: <T> is a type parameter, here bounded by T: Copy so the value can be returned by copy. Rust monomorphizes generics — it generates a specialized version per concrete type — so there is no runtime cost. PHP can only approximate this with @template docblocks for static analysers.
Generic Structs
<?php // A PHP container just holds mixed values: class Wrapper { public function __construct(public mixed $value) {} } $wrapped = new Wrapper(42); echo $wrapped->value, "\n"; // 42
struct Wrapper<T> { value: T, } impl<T: std::fmt::Display> Wrapper<T> { fn show(&self) { println!("{}", self.value); } } Wrapper { value: 42 }.show(); // 42 Wrapper { value: "hi" }.show(); // hi
Structs can be generic too: Wrapper<T> holds a value of any type, and the impl block can require trait bounds like T: Display so the body may print it. This gives you type-safe containers — what the standard library's Vec<T> and Option<T> are built on — with no PHP equivalent.
Error Handling
Result Instead of Exceptions
<?php function parseAmount(string $input): int { if (!is_numeric($input)) { throw new InvalidArgumentException("not a number: {$input}"); } return (int) $input; } try { echo parseAmount("42"), "\n"; // 42 parseAmount("abc"); } catch (InvalidArgumentException $error) { echo "Caught: ", $error->getMessage(), "\n"; }
fn parse_amount(input: &str) -> Result<i32, String> { input.parse::<i32>().map_err(|_| format!("not a number: {input}")) } match parse_amount("42") { Ok(value) => println!("{value}"), // 42 Err(message) => println!("Caught: {message}"), } match parse_amount("abc") { Ok(value) => println!("{value}"), Err(message) => println!("Caught: {message}"), }
Rust has no exceptions. A fallible operation returns a Result<T, E> — either Ok(value) or Err(error) — and the compiler forces you to handle both, so an error can never be silently ignored. This is the opposite of PHP's throw-and-maybe-catch model, where an uncaught exception unwinds at runtime.
The ? Operator
<?php // PHP propagates errors by letting exceptions bubble up: function readNumber(string $input): int { return parseValue($input) + 1; // if parseValue throws, it propagates } function parseValue(string $input): int { if (!is_numeric($input)) { throw new InvalidArgumentException("bad: {$input}"); } return (int) $input; } try { echo readNumber("41"), "\n"; // 42 } catch (InvalidArgumentException $error) { echo $error->getMessage(), "\n"; }
fn parse_value(input: &str) -> Result<i32, std::num::ParseIntError> { input.parse::<i32>() } fn read_number(input: &str) -> Result<i32, std::num::ParseIntError> { let value = parse_value(input)?; // ? returns early on Err Ok(value + 1) } match read_number("41") { Ok(value) => println!("{value}"), // 42 Err(error) => println!("{error}"), }
The ? operator is Rust's ergonomic error propagation: parse_value(input)? unwraps an Ok or returns the Err from the enclosing function immediately. It gives you the "let it bubble up" convenience of PHP exceptions while keeping every error path visible in the type signatures.
panic! for Unrecoverable Errors
<?php // An uncaught PHP exception terminates the script with an error: function mustBePositive(int $value): int { if ($value <= 0) { throw new RuntimeException("expected positive, got {$value}"); } return $value; } echo mustBePositive(5), "\n"; // 5
fn must_be_positive(value: i32) -> i32 { if value <= 0 { panic!("expected positive, got {value}"); } value } println!("{}", must_be_positive(5)); // 5
For genuinely unrecoverable bugs, panic! aborts the current thread with a message, much like an uncaught exception ending a PHP script. The distinction Rust draws is deliberate: use Result for expected, recoverable failures and reserve panic! for "this should never happen" invariants.
⚠ Gotchas for PHP Devs
Integer Types & Overflow
<?php // PHP ints are 64-bit and silently become floats on overflow: $big = PHP_INT_MAX; echo $big, "\n"; echo $big + 1, "\n"; // becomes a float (9.2233720368548E+18)
let big: i32 = 2_000_000_000; // big + big would panic in debug builds (overflow) — be explicit instead: let wrapped = big.wrapping_add(big); // wraps around let checked = big.checked_add(big); // returns Option (None on overflow) println!("{wrapped}"); println!("{:?}", checked); // None
Rust's integers are fixed-width and typed (i32, u64, …), and overflow is a real concern: it panics in debug builds rather than silently promoting to a float like PHP. You choose the behavior explicitly with methods like wrapping_add, checked_add, or saturating_add.
No Implicit Type Coercion
<?php // PHP freely juggles types in arithmetic and comparison: echo "5" + 3, "\n"; // 8 (string coerced to int) echo 5 . 3, "\n"; // 53 (numbers coerced to string) var_dump(5 == "5"); // true
let count: i64 = 5; let scaled: f64 = 2.0; // let bad = count * scaled; // compile error — i64 and f64 don't mix let mixed = count as f64 * scaled; // explicit cast with 'as' println!("{mixed}"); // 10
Rust performs no implicit numeric coercion — you cannot even multiply an i64 by an f64 without an explicit as cast, and there is no string/number juggling at all. This eliminates the "5" + 3 surprises of PHP, at the cost of writing the conversions yourself.
There Is No null
<?php // In PHP any variable can be null, and forgetting to check is a common bug: function lastChar(?string $text): string { return $text[strlen($text) - 1]; // crashes if $text is null } echo lastChar("hello"), "\n"; // o
fn last_char(text: Option<&str>) -> Option<char> { text?.chars().last() // ? short-circuits to None if absent } println!("{:?}", last_char(Some("hello"))); // Some('o') println!("{:?}", last_char(None)); // None
Because Rust has no null, the "forgot to check for null" bug simply cannot occur — absence is modeled as Option and the compiler refuses to let you use the inner value without handling None. The ? operator works on Option too, making the safe path the short one.
The Borrow Checker
<?php // PHP lets you mutate a collection while iterating it (sometimes buggily): $numbers = [1, 2, 3]; foreach ($numbers as $number) { // modifying $numbers here is allowed but error-prone } $numbers[] = 4; echo count($numbers), "\n"; // 4
let mut numbers = vec![1, 2, 3]; // This would be rejected — can't push while a borrow from iteration is live: // for n in &numbers { numbers.push(*n); } // compile error for n in &numbers { print!("{n} "); // read-only borrow, fine } println!(); numbers.push(4); // OK — the borrow has ended println!("{}", numbers.len()); // 4
The borrow checker enforces that you cannot mutate a collection while an iteration borrow over it is still alive — a class of aliasing bug PHP allows at runtime. Fighting the borrow checker is the usual early-Rust experience; it is the compiler proving your memory and aliasing are sound before the program ever runs.