PONY λ M2 Modula-2

Ruby.CodeCompared.To/Mojo

An interactive executable cheatsheet for Rubyists learning Mojo

Ruby 4.0 Mojo 0.26.1
Output & Basics
Hello World
puts "Hello, World!"
fn main() raises: print("Hello, World!")

Every Mojo program starts with fn main(), the required entry point. The raises qualifier allows the function to propagate errors — it is a good habit to include it so that any function called from main can use raise freely. Mojo's print() works like Python's: it adds a newline after each call, just like Ruby's puts.

Multiple outputs
puts "First" puts "Second" puts "Third"
fn main() raises: print("First") print("Second") print("Third")

Mojo uses Python-style significant indentation. Statements inside a function body must be indented consistently. Each print() call outputs one line, like Ruby's puts.

Printing multiple values
name = "Alice" age = 30 puts "#{name} is #{age} years old"
fn main() raises: var name = "Alice" var age = 30 print(name, "is", age, "years old")

Mojo's print() accepts multiple arguments and joins them with spaces, similar to Python. Mojo 0.26 does not have string interpolation syntax like Ruby's #{} or Python's f-strings; the idiomatic approach is to pass multiple values directly to print().

Comments
# Single-line comment puts "Hello" # inline comment =begin Multi-line comment block =end
fn main() raises: # Single-line comment print("Hello") # inline comment # Mojo has no block comment syntax; # use consecutive line comments instead.

Mojo uses # for line comments, identical to Ruby and Python. Unlike Ruby, Mojo has no built-in block comment syntax such as =begin / =end. Multiple consecutive line comments are the standard approach for longer explanatory text.

Variables & Types
Type inference
name = "Alice" count = 42 temperature = 98.6 puts name, count, temperature
fn main() raises: var name = "Alice" var count = 42 var temperature = 98.6 print(name, count, temperature)

Mojo requires the var keyword to declare a new variable — unlike Ruby, where bare assignment creates a variable implicitly. The type is inferred from the initial value, so var count = 42 creates an Int without an explicit annotation.

Type annotations
# Ruby has no built-in type annotation syntax name = "Alice" age = 30 height = 5.9 puts "#{name}, #{age}, #{height}"
fn main() raises: var name: String = "Alice" var age: Int = 30 var height: Float64 = 5.9 print(name, age, height)

Mojo supports optional type annotations in the form var name: Type = value. The annotation is redundant when the type can be inferred, but it serves as documentation and will cause a compile error if the assigned value does not match. Ruby has no equivalent built-in syntax.

Reassignment and mutation
count = 0 count = 10 count += 5 puts count
fn main() raises: var count = 0 count = 10 count += 5 print(count)

var variables in Mojo are mutable by default and can be reassigned freely, just like local variables in Ruby. Compound assignment operators like +=, -=, and *= are all supported.

Constants (alias)
MAX_CONNECTIONS = 100 APP_NAME = "MyApp" puts MAX_CONNECTIONS puts APP_NAME
alias MAX_CONNECTIONS: Int = 100 alias APP_NAME: String = "MyApp" fn main() raises: print(MAX_CONNECTIONS) print(APP_NAME)

alias creates a compile-time constant, similar to Ruby's capitalized constants. Because alias values are resolved at compile time rather than runtime, they can be used in places that require compile-time known values — for example, as SIMD type parameters. Note that alias with an explicit type annotation is deprecated in some Mojo versions but remains functional in 0.26.

Type conversion
whole = 10 decimal = whole.to_f / 3.0 text = whole.to_s puts decimal puts text + " items"
fn main() raises: var whole: Int = 10 var decimal = Float64(whole) / 3.0 var text = String(whole) print(decimal) print(text + " items")

Mojo does not perform implicit type coercion between numeric types. Converting an Int to a Float64 requires an explicit constructor call such as Float64(whole), similar to Ruby's .to_f. Converting to String uses String(value), like Ruby's .to_s.

Type System
Integer types
# Ruby has one Integer type (arbitrary precision) small = 42 large = 1_000_000_000 puts small.class puts large
fn main() raises: var machine_word: Int = 42 var signed_64: Int64 = 1_000_000_000 var signed_32: Int32 = 32_767 print(machine_word) print(signed_64) print(signed_32)

Mojo has multiple integer types sized to specific bit widths: Int8, Int16, Int32, Int64, and their unsigned counterparts. The plain Int type is machine-word-sized (64-bit on a 64-bit system) and is the most common choice. Unlike Ruby, which uses a single arbitrary-precision Integer, Mojo integers can overflow.

Float types
pi = 3.14159265358979 small_pi = 3.14 puts pi puts small_pi.class
fn main() raises: var pi: Float64 = 3.14159265358979 var approximate: Float32 = 3.14 print(pi) print(approximate)

Mojo provides Float16, Float32, and Float64 types. The default floating-point literal type is Float64, equivalent to Ruby's Float. Use Float32 when you need to reduce memory usage or take advantage of SIMD hardware that prefers 32-bit floats.

Bool type
is_active = true is_done = false puts is_active puts !is_active puts is_active && !is_done
fn main() raises: var is_active: Bool = True var is_done: Bool = False print(is_active) print(not is_active) print(is_active and not is_done)

Mojo's Bool type uses Python-style capitalized literals True and False, unlike Ruby's lowercase true and false. Boolean operators use English words (and, or, not) rather than symbols, which is another Python-style difference from Ruby's &&, ||, !.

Optional (nullable) values
def find_score(scores, name) scores[name] # returns nil if not found end scores = { "Alice" => 95 } result = find_score(scores, "Bob") puts result.nil? ? "Not found" : result
fn safe_parse(text: String) -> Optional[Int]: if text == "42": return 42 return None fn main() raises: var found = safe_parse("42") if found: print(found.value()) var missing = safe_parse("abc") print(missing.or_else(0))

Optional[T] is Mojo's equivalent of Ruby's nilable references. A function returning Optional[T] can return either a value or None. Use .value() to extract the value when you know it is present, or .or_else(default) to provide a fallback — similar to Ruby's &. operator and || default pattern.

Strings
String basics
greeting = "Hello" name = "World" combined = greeting + ", " + name + "!" puts combined puts combined.length
fn main() raises: var greeting = "Hello" var name = "World" var combined = greeting + ", " + name + "!" print(combined) print(len(combined))

String concatenation uses + in both Ruby and Mojo. Mojo uses the built-in len() function for string length, inherited from Python's style, whereas Ruby uses the .length or .size method. All Mojo strings are UTF-8 encoded and immutable by default.

Case conversion
message = "Hello, World!" puts message.upcase puts message.downcase
fn main() raises: var message = "Hello, World!" print(message.upper()) print(message.lower())

Mojo's String type uses Python-style method names: .upper() and .lower() rather than Ruby's .upcase and .downcase. Both languages return a new string; the original is not modified.

Find and replace
sentence = "The quick brown fox" puts sentence.include?("quick") puts sentence.index("fox") puts sentence.gsub("fox", "cat")
fn main() raises: var sentence = "The quick brown fox" print(sentence.find("quick") != -1) print(sentence.find("fox")) print(sentence.replace("fox", "cat"))

Mojo's .find() returns the byte index of the first occurrence, or -1 if not found — similar to Ruby's .index(). Use != -1 to check for containment; Mojo does not have a separate .include? method. The .replace() method replaces all occurrences, like Ruby's .gsub.

Strip and split
padded = " hello " puts padded.strip words = "one two three".split puts words.length
fn main() raises: var padded = " hello " print(padded.strip()) var sentence = "one two three" var words = sentence.split() print(len(words))

.strip() removes leading and trailing whitespace, equivalent to Ruby's .strip. .split() without arguments splits on whitespace and returns a List[String], like Ruby's .split. Both methods follow Python naming conventions rather than Ruby's.

Prefix and suffix checks
filename = "report.csv" puts filename.start_with?("report") puts filename.end_with?(".csv") puts filename.end_with?(".txt")
fn main() raises: var filename = "report.csv" print(filename.startswith("report")) print(filename.endswith(".csv")) print(filename.endswith(".txt"))

Mojo uses Python-style method names .startswith() and .endswith(), compared to Ruby's .start_with? and .end_with?. The Mojo versions return a Bool value and follow the same semantics.

Collections
List basics
numbers = [10, 20, 30, 40] puts numbers[0] puts numbers[-1] puts numbers.length
fn main() raises: var numbers = List[Int]() numbers.append(10) numbers.append(20) numbers.append(30) numbers.append(40) print(numbers[0]) print(numbers[len(numbers) - 1]) print(len(numbers))

Mojo's List[T] is a typed, resizable array. Unlike Ruby's array literals ([1, 2, 3]), Mojo requires explicit construction and type parameter. Mojo lists are zero-indexed like Ruby's, but Mojo 0.26 does not support negative indices — use len(list) - 1 for the last element.

List iteration
fruits = ["apple", "banana", "cherry"] fruits.each { |fruit| puts fruit } fruits.each_with_index do |fruit, index| puts "#{index}: #{fruit}" end
fn main() raises: var fruits = List[String]() fruits.append("apple") fruits.append("banana") fruits.append("cherry") for fruit in fruits: print(fruit) for index in range(len(fruits)): print(index, fruits[index])

Iterating over a Mojo List with a for loop yields the element value directly. Index-based iteration using range(len(list)) also works and gives access to the index, similar to Ruby's each_with_index. Direct subscript access (list[index]) returns the value without any dereference step.

Dict basics
scores = { "Alice" => 95, "Bob" => 87 } puts scores["Alice"] scores["Charlie"] = 92 puts scores.length
fn main() raises: var scores = Dict[String, Int]() scores["Alice"] = 95 scores["Bob"] = 87 print(scores["Alice"]) scores["Charlie"] = 92 print(len(scores))

Dict[K, V] is Mojo's typed hash map, equivalent to Ruby's Hash. Like Ruby, subscript assignment (dict["key"] = value) adds or updates entries, and subscript access returns the value. Accessing a missing key in Mojo raises an error at runtime, whereas Ruby returns nil — use .get() for safe access.

Safe dict access
config = { "host" => "localhost", "port" => "5432" } host = config.fetch("host", "127.0.0.1") timeout = config.fetch("timeout", "30") puts host puts timeout
fn main() raises: var config = Dict[String, String]() config["host"] = "localhost" config["port"] = "5432" var host = config.get("host", "127.0.0.1") var timeout = config.get("timeout", "30") print(host) print(timeout)

Dict.get(key, default) returns the value for key if present, otherwise returns the default — equivalent to Ruby's Hash#fetch(key, default). This is the safe alternative to subscript access, which raises an error when the key is missing.

Dict iteration
grades = { "Alice" => "A", "Bob" => "B", "Charlie" => "C" } grades.each do |name, grade| puts "#{name}: #{grade}" end
fn main() raises: var grades = Dict[String, String]() grades["Alice"] = "A" grades["Bob"] = "B" grades["Charlie"] = "C" for key in grades.keys(): var name = String(key) print(name, grades[name])

Iterating over dict.keys() yields key values that can be used for lookup. Because the iterator holds a reference into the dict's own memory, accessing grades[key] while iterating can trigger an aliasing error — copy the key first with String(key) to get an independent value. Mojo 0.26 does not guarantee dictionary insertion order.

Control Flow
if / elif / else
score = 85 if score >= 90 puts "A" elsif score >= 80 puts "B" elsif score >= 70 puts "C" else puts "F" end
fn main() raises: var score = 85 if score >= 90: print("A") elif score >= 80: print("B") elif score >= 70: print("C") else: print("F")

Mojo uses Python-style elif instead of Ruby's elsif. Conditional blocks are delimited by indentation rather than end keywords. The comparison operators (>=, ==, !=) are identical to Ruby's.

for loop with range
(1..5).each { |number| puts number } (0...10).step(2) { |number| puts number }
fn main() raises: for number in range(1, 6): print(number) for number in range(0, 10, 2): print(number)

Mojo uses Python's range(start, stop, step) for numeric loops. The stop value is exclusive, like Ruby's ... range. The three-argument form sets the step. Unlike Ruby, there is no step method on ranges — the step is a third argument to range().

while loop
countdown = 5 while countdown > 0 puts countdown countdown -= 1 end puts "Go!"
fn main() raises: var countdown = 5 while countdown > 0: print(countdown) countdown -= 1 print("Go!")

Mojo's while loop is syntactically identical to Python's. The loop body is delimited by indentation rather than Ruby's do / end or braces. Unlike Ruby, Mojo has no until keyword — use while not condition: instead.

break and continue
(1..10).each do |number| next if number % 2 == 0 break if number > 7 puts number end
fn main() raises: for number in range(1, 11): if number % 2 == 0: continue if number > 7: break print(number)

Mojo uses continue (Python-style) for what Ruby calls next, and break is the same in both languages. Unlike Ruby, Mojo's break cannot return a value — it only exits the loop.

Looping with an index
colors = ["red", "green", "blue"] colors.each_with_index do |color, index| puts "#{index}: #{color}" end
fn main() raises: var colors = List[String]() colors.append("red") colors.append("green") colors.append("blue") for index in range(len(colors)): print(index, colors[index])

Mojo 0.26 does not have a built-in equivalent of Ruby's each_with_index or Python's enumerate() for lists. The standard approach is to iterate over range(len(collection)) and access elements by index. Both for element in list: and list[index] give you the element value directly.

def Functions
Basic def function
def greet(name) "Hello, #{name}!" end puts greet("Alice")
def greet(name: String) -> String: return "Hello, " + name + "!" fn main() raises: print(greet("Alice"))

Mojo's def functions are Python-style: they support type annotations but do not require them, and can raise errors without declaring raises. Unlike fn functions, def functions may implicitly copy arguments and can use dynamic types. Ruby's def is the closest analogy — both are flexible and dynamically typed by default.

Default arguments
def greet(name = "World", punctuation = "!") "Hello, #{name}#{punctuation}" end puts greet puts greet("Alice") puts greet("Bob", ".")
def greet(name: String = "World", punctuation: String = "!") -> String: return "Hello, " + name + punctuation fn main() raises: print(greet()) print(greet("Alice")) print(greet("Bob", "."))

Default argument syntax is nearly identical between Ruby and Mojo's def functions. The Mojo version requires type annotations alongside default values. Unlike Ruby, Mojo does not support keyword-only arguments in def functions (you cannot call greet(punctuation: ".")).

Returning multiple values
def min_max(numbers) [numbers.min, numbers.max] end low, high = min_max([3, 1, 4, 1, 5, 9, 2]) puts low puts high
def min_max(first: Int, second: Int, third: Int) -> Tuple[Int, Int]: var minimum = first var maximum = first if second < minimum: minimum = second if second > maximum: maximum = second if third < minimum: minimum = third if third > maximum: maximum = third return (minimum, maximum) fn main() raises: var result = min_max(3, 7, 1) print(result[0]) print(result[1])

Mojo functions return multiple values via Tuple[T1, T2], accessed by numeric index (result[0]). Ruby returns multiple values as an array, which can be destructured automatically. Mojo 0.26 does not support automatic tuple destructuring in assignment (var low, high = min_max(...) is not valid syntax).

Recursive functions
def factorial(number) return 1 if number <= 1 number * factorial(number - 1) end puts factorial(6)
def factorial(number: Int) -> Int: if number <= 1: return 1 return number * factorial(number - 1) fn main() raises: print(factorial(6))

Mojo supports recursion in both def and fn functions. The recursive call syntax is identical to Ruby. Unlike Ruby, Mojo does not perform tail-call optimization, so deeply recursive calls can overflow the stack.

fn Functions
Strict fn function
# Ruby has no equivalent of fn; all methods are dynamically typed def add(x, y) x + y end puts add(3, 4) puts add(1.5, 2.5)
fn add(left: Int, right: Int) -> Int: return left + right fn main() raises: print(add(3, 4))

fn functions require explicit type annotations for all parameters and the return type. They are stricter than def functions: arguments are not copied implicitly, borrowed by default. Where a Ruby method accepts any type, an fn function is locked to specific types — you would need a separate overload to handle Float64 arguments.

Mutable parameters (mut)
# Ruby passes objects by reference-value; modifying inside is visible outside def double_in_place(numbers) numbers.map! { |n| n * 2 } end numbers = [1, 2, 3] double_in_place(numbers) puts numbers.inspect
fn double_in_place(mut numbers: List[Int]): for index in range(len(numbers)): numbers[index] = numbers[index] * 2 fn main() raises: var numbers = List[Int]() numbers.append(1) numbers.append(2) numbers.append(3) double_in_place(numbers) for number in numbers: print(number)

The mut keyword on a parameter declares that the function can modify the caller's variable. Without mut, the parameter is borrowed immutably and cannot be changed. This is explicit where Ruby implicitly shares mutable object references — in Mojo, the mutation contract is visible at the call site.

Functions that can raise
def parse_age(text) age = Integer(text) raise ArgumentError, "Age cannot be negative" if age < 0 age rescue ArgumentError => error raise error end puts parse_age("25")
fn parse_age(text: String) raises -> Int: var age = atol(text) if age < 0: raise Error("Age cannot be negative") return age fn main() raises: print(parse_age("25"))

An fn function that can raise an error must declare raises in its signature, unlike Ruby where any method can raise at any time. This makes the error contract explicit and visible to callers. The atol() built-in (ASCII to long) converts a String to Int and itself raises if parsing fails.

Function overloading
# Ruby uses duck typing instead of overloading def describe(value) if value.is_a?(Integer) "Integer: #{value}" elsif value.is_a?(Float) "Float: #{value}" else "String: #{value}" end end puts describe(42) puts describe(3.14)
fn describe(value: Int) -> String: return "Integer: " + String(value) fn describe(value: Float64) -> String: return "Float: " + String(value) fn main() raises: print(describe(42)) print(describe(3.14))

Mojo supports function overloading: multiple functions with the same name but different parameter types. The compiler selects the correct version at compile time based on the argument types. Ruby achieves similar behavior through duck typing and is_a? checks at runtime — Mojo does the same work statically.

Structs
Struct with auto-initializer
Point = Struct.new(:x, :y) point = Point.new(3.0, 4.0) puts point.x puts point.y
@fieldwise_init struct Point(Copyable, Movable): var x: Float64 var y: Float64 fn main() raises: var point = Point(3.0, 4.0) print(point.x) print(point.y)

The @fieldwise_init decorator generates a constructor that accepts one argument per field in declaration order. Copyable and Movable are built-in traits that allow the struct to be copied and moved. This is similar to Ruby's Struct.new, which also auto-generates accessors and a constructor. Mojo structs are value types — assigning one struct to another copies it, unlike Ruby objects which are always reference types.

Struct with explicit initializer
class Rectangle attr_reader :width, :height def initialize(width, height) @width = width @height = height end end rect = Rectangle.new(3.0, 4.0) puts rect.width
struct Rectangle: var width: Float64 var height: Float64 fn __init__(out self, width: Float64, height: Float64): self.width = width self.height = height fn main() raises: var rectangle = Rectangle(3.0, 4.0) print(rectangle.width)

Mojo structs use fn __init__(out self, ...) for explicit constructors. The out self parameter indicates that the function constructs and initializes the struct — it is Mojo's equivalent of Ruby's initialize. Fields are accessed via self.field_name inside the struct, and instance.field_name from outside (like Ruby's instance variables with attr_reader).

Struct instance methods
class Circle def initialize(radius) @radius = radius end def area Math::PI * @radius ** 2 end def circumference 2 * Math::PI * @radius end end circle = Circle.new(5.0) puts circle.area.round(4)
from math import pi struct Circle: var radius: Float64 fn __init__(out self, radius: Float64): self.radius = radius fn area(self) -> Float64: return pi * self.radius * self.radius fn circumference(self) -> Float64: return 2.0 * pi * self.radius fn main() raises: var circle = Circle(5.0) print(circle.area())

Read-only methods take self (borrowed, immutable). Mojo imports standard library functions like pi explicitly — unlike Ruby, where Math::PI is always available. Methods are called with instance.method(), always requiring parentheses even for zero-argument methods (unlike Ruby).

Mutating methods
class Counter def initialize @count = 0 end def increment @count += 1 end def value @count end end counter = Counter.new counter.increment counter.increment counter.increment puts counter.value
struct Counter: var count: Int fn __init__(out self): self.count = 0 fn increment(mut self): self.count += 1 fn value(self) -> Int: return self.count fn main() raises: var counter = Counter() counter.increment() counter.increment() counter.increment() print(counter.value())

Methods that modify the struct's state must declare mut self instead of self. This is explicit where Ruby has no distinction — any Ruby method can modify @instance_variables. In Mojo, calling a mut self method on an immutable binding would be a compile error, making mutation intent visible in the type signature.

String representation
class Person def initialize(name, age) @name = name @age = age end def to_s "Person(#{@name}, #{@age})" end end person = Person.new("Alice", 30) puts person
struct Person(Stringable): var name: String var age: Int fn __init__(out self, name: String, age: Int): self.name = name self.age = age fn __str__(self) -> String: return "Person(" + self.name + ", " + String(self.age) + ")" fn main() raises: var person = Person("Alice", 30) print(String(person))

Implementing the Stringable trait and defining fn __str__(self) -> String gives a struct a string representation, analogous to Ruby's to_s method. Use String(instance) to invoke __str__ and get the string — Mojo 0.26 does not expose a standalone str() function. Passing the struct directly to print() requires implementing the Writable trait instead.

Traits
Defining a trait
module Greetable def greet raise NotImplementedError, "#{self.class} must implement greet" end end module Farewell def farewell "Goodbye from #{self.class}" end end
trait Greetable: fn greet(self) -> String: ... trait Printable: fn display(self): ... fn main() raises: pass

A Mojo trait declares a set of required method signatures — similar to Ruby modules used as interfaces, but with compile-time enforcement. The ... body means "no default implementation; implementors must define this." Unlike Ruby modules, Mojo traits cannot contain instance variables or default method bodies in 0.26.

Implementing a trait
module Describable def describe raise NotImplementedError end end class Dog include Describable def initialize(name) @name = name end def describe "Dog named #{@name}" end end dog = Dog.new("Rex") puts dog.describe
trait Describable: fn describe(self) -> String: ... @fieldwise_init struct Dog(Describable, Copyable, Movable): var name: String fn describe(self) -> String: return "Dog named " + self.name fn main() raises: var dog = Dog("Rex") print(dog.describe())

A struct implements a trait by listing it in parentheses after the struct name (struct Dog(Describable, ...)) and providing all required methods. The compiler verifies that every trait method is implemented — unlike Ruby, where missing included methods only raise at runtime when called. Multiple traits can be listed together with built-in traits like Copyable and Movable.

Polymorphism via traits
module Shape def area raise NotImplementedError end end class Square include Shape def initialize(side) = @side = side def area = @side ** 2 end class Circle include Shape def initialize(radius) = @radius = radius def area = Math::PI * @radius ** 2 end [Square.new(4.0), Circle.new(3.0)].each { |shape| puts shape.area.round(4) }
from math import pi trait Shape: fn area(self) -> Float64: ... @fieldwise_init struct Square(Shape, Copyable, Movable): var side: Float64 fn area(self) -> Float64: return self.side * self.side @fieldwise_init struct Circle(Shape, Copyable, Movable): var radius: Float64 fn area(self) -> Float64: return pi * self.radius * self.radius fn print_area[T: Shape](shape: T): print(shape.area()) fn main() raises: var square = Square(4.0) var circle = Circle(3.0) print_area(square) print_area(circle)

Mojo achieves polymorphism via parametric functions: fn print_area[T: Shape](shape: T) accepts any type T that implements Shape. The square brackets [T: Shape] are compile-time type parameters — the compiler generates a specialized version for each concrete type, like C++ templates. This differs from Ruby's runtime duck typing, but produces the same behavior with zero runtime overhead.

Built-in traits
class Temperature include Comparable attr_reader :celsius def initialize(celsius) @celsius = celsius end def <=>(other) @celsius <=> other.celsius end end temps = [Temperature.new(100), Temperature.new(37), Temperature.new(0)] puts temps.min.celsius puts temps.sort.map(&:celsius).inspect
@fieldwise_init struct Temperature(Copyable, Movable, Comparable): var celsius: Float64 fn __lt__(self, other: Temperature) -> Bool: return self.celsius < other.celsius fn __le__(self, other: Temperature) -> Bool: return self.celsius <= other.celsius fn __gt__(self, other: Temperature) -> Bool: return self.celsius > other.celsius fn __ge__(self, other: Temperature) -> Bool: return self.celsius >= other.celsius fn __eq__(self, other: Temperature) -> Bool: return self.celsius == other.celsius fn __ne__(self, other: Temperature) -> Bool: return self.celsius != other.celsius fn main() raises: var boiling = Temperature(100.0) var body = Temperature(37.0) var freezing = Temperature(0.0) print(boiling > body) print(freezing < body)

Mojo's built-in traits include Copyable, Movable, Stringable, Comparable, and others. The Comparable trait requires implementing all six comparison dunder methods. Ruby achieves the same result by implementing only <=> and including Comparable — Mojo requires explicit implementations for each operator.

Error Handling
Raising errors
def check_positive(number) raise ArgumentError, "Must be positive" if number <= 0 number end puts check_positive(5)
fn check_positive(number: Int) raises -> Int: if number <= 0: raise Error("Must be positive") return number fn main() raises: print(check_positive(5))

Mojo's raise Error("message") is the equivalent of Ruby's raise. Unlike Ruby's hierarchy of exception classes (ArgumentError, RuntimeError, etc.), Mojo 0.26 has a single Error type. The function signature must declare raises whenever it can raise, making the error contract part of the public API.

try / except
def risky_divide(dividend, divisor) raise ZeroDivisionError, "Cannot divide by zero" if divisor == 0 dividend.to_f / divisor end begin puts risky_divide(10, 2) puts risky_divide(5, 0) rescue ZeroDivisionError => error puts "Caught: #{error.message}" end
fn risky_divide(dividend: Float64, divisor: Float64) raises -> Float64: if divisor == 0.0: raise Error("Cannot divide by zero") return dividend / divisor fn main() raises: try: print(risky_divide(10.0, 2.0)) print(risky_divide(5.0, 0.0)) except error: print("Caught:", error)

Mojo uses try / except (Python-style) instead of Ruby's begin / rescue. The caught value in except error: is of type Error. Unlike Ruby, Mojo cannot catch specific error subtypes — all raised errors are the same Error type in 0.26, so there is no equivalent of rescue ZeroDivisionError.

Propagating errors
def read_config(path) raise IOError, "File not found: #{path}" unless File.exist?(path) "config content" end def initialize_app config = read_config("app.yaml") "App initialized with #{config}" end begin puts initialize_app rescue IOError => error puts "Setup failed: #{error.message}" end
fn read_config(path: String) raises -> String: if path != "app.yaml": raise Error("File not found: " + path) return "config content" fn initialize_app() raises -> String: var config = read_config("app.yaml") return "App initialized with " + config fn main() raises: try: print(initialize_app()) except error: print("Setup failed:", error)

When a function marked raises calls another raises function without a try block, the error propagates automatically up the call stack — identical to Ruby's default behavior. The raises annotation on each function in the chain makes the propagation path explicit and compiler-verified.

Cleanup with else
def process_data(data) raise "Empty data" if data.empty? data.upcase end begin result = process_data("hello") puts "Success: #{result}" rescue => error puts "Error: #{error.message}" end
fn process_data(data: String) raises -> String: if len(data) == 0: raise Error("Empty data") return data.upper() fn main() raises: try: var result = process_data("hello") print("Success:", result) except error: print("Error:", error)

Mojo 0.26 does not have an ensure / finally clause equivalent. Error handling uses try / except blocks. Cleanup logic that must run regardless of success or failure is typically placed after the try block, or managed via Mojo's lifetime system using destructors (fn __del__(owned self)).

Errors in struct methods
class BankAccount def initialize(balance) @balance = balance end def withdraw(amount) raise "Insufficient funds" if amount > @balance @balance -= amount @balance end end account = BankAccount.new(100) puts account.withdraw(40)
struct BankAccount: var balance: Float64 fn __init__(out self, balance: Float64): self.balance = balance fn withdraw(mut self, amount: Float64) raises -> Float64: if amount > self.balance: raise Error("Insufficient funds") self.balance -= amount return self.balance fn main() raises: var account = BankAccount(100.0) print(account.withdraw(40.0))

Struct methods can be declared raises just like standalone functions. A method that both mutates the struct and can raise combines mut self with raises in the signature. This mirrors Ruby's pattern of instance methods that modify state and raise on invalid input.

Performance Features
SIMD vectors
# Ruby has no built-in SIMD; this shows equivalent serial computation prices = [10.0, 20.0, 30.0, 40.0] discounted = prices.map { |price| price * 0.9 } puts discounted.inspect
fn main() raises: var prices = SIMD[DType.float64, 4](10.0, 20.0, 30.0, 40.0) var discounted = prices * 0.9 print(discounted)

SIMD[DType, width] is a vector type that maps directly to CPU SIMD instructions. Operations like * 0.9 are applied to all elements in a single instruction, rather than Ruby's sequential loop. The width must be a power of two and match what the target hardware supports (4 or 8 Float64 elements is typical on x86-64). This is one of Mojo's headline features — C-level performance in high-level syntax.

SIMD arithmetic and reduction
# Ruby serial operations vector_a = [1.0, 2.0, 3.0, 4.0] vector_b = [5.0, 6.0, 7.0, 8.0] total = vector_a.zip(vector_b).sum { |a, b| a + b } puts total
fn main() raises: var vector_a = SIMD[DType.float64, 4](1.0, 2.0, 3.0, 4.0) var vector_b = SIMD[DType.float64, 4](5.0, 6.0, 7.0, 8.0) var combined = vector_a + vector_b var total = combined.reduce_add() print(total)

SIMD types support all standard arithmetic operators element-wise: +, -, *, /. The .reduce_add() method sums all elements in the vector into a scalar, equivalent to Ruby's .sum. On modern hardware, the 4-element addition is performed in a single CPU instruction instead of four separate additions.

Compile-time parameters
# Ruby determines array size at runtime def process_batch(items) items.each_slice(4).map { |batch| batch.sum } end puts process_batch([1, 2, 3, 4, 5, 6, 7, 8]).inspect
alias BATCH_SIZE: Int = 4 fn sum_batch[size: Int](values: SIMD[DType.int64, size]) -> Int64: return values.reduce_add() fn main() raises: var batch = SIMD[DType.int64, BATCH_SIZE](10, 20, 30, 40) print(sum_batch[BATCH_SIZE](batch))

Square-bracket parameters like [size: Int] are compile-time parameters. They are resolved when the compiler generates code, unlike function arguments which are resolved at runtime. Using alias constants as type parameters (like SIMD[DType.int64, BATCH_SIZE]) allows the vector width to be defined once and reused without any runtime overhead.

Python interoperability
# Ruby can call C extensions, but not Python libraries directly require "json" data = JSON.parse('{"name": "Alice"}') puts data["name"]
from python import Python fn main() raises: var json = Python.import_module("json") var data = json.loads('{"name": "Alice"}') print(data["name"])

Mojo can import and call any Python library directly via from python import Python and Python.import_module(). This requires a CPython installation to be present at runtime. The example above cannot run in Compiler Explorer because the CE sandbox does not include a Python runtime — it works in a full Mojo installation. Ruby has no equivalent facility for calling Python code.