PONY λ M2 Modula-2

Perl.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Perl and Ruby

Perl 5.40 Ruby 4.0
Hello World & Running
Hello, World
use v5.38; say "Hello, World!";
puts "Hello, World!"
Ruby needs no use v5.38; preamble and no trailing semicolon — a newline ends a statement. puts is the direct equivalent of Perl's say: it prints a value followed by a newline. Ruby has no distinct "modern mode" to opt into; the behavior Perl gates behind use v5.38 is simply how Ruby always works.
Running a program
# Run directly (no compile step): # perl hello.pl # # Check syntax only: # perl -c hello.pl # # One-liner: # perl -e 'print "Hello!\n"'
# Run directly (no compile step): # ruby hello.rb # # Run a one-liner: # ruby -e 'puts "Hello!"' # # Interactive REPL: # irb
Both languages are interpreted with no separate compile step, so ruby hello.rb and perl hello.pl feel identical to run. The difference shows up in exploration: Ruby ships irb, a full interactive REPL for trying expressions one at a time, where Perl programmers typically reach for a one-liner (perl -e) or the separate Devel::REPL module instead.
Comments
use v5.38; # Single-line comment — the only inline comment syntax # Perl has no multi-line comment syntax. # Block documentation uses Pod: =begin comment ... =end comment my $name = "Alice"; # inline comment say $name;
# Single-line comment =begin Multi-line comment =end name = "Alice" # inline comment puts name
Both languages use # for single-line comments. Ruby additionally supports an =begin / =end block comment, borrowed directly from Perl's Pod (Plain Old Documentation) markers — but it is rarely used in practice; consecutive # lines are the idiomatic style in both languages for multi-line commentary.
print vs say vs p
use v5.38; print "no newline"; print "\n"; say "with newline"; # say appends a newline automatically say 42; # works with numbers too
puts "with newline" # like Perl's say print "no newline" # like Perl's print puts # blank line p "hello" # inspect: shows "hello" with quotes p [1, 2, 3] # inspect: shows [1, 2, 3]
Ruby's puts is exactly Perl's say — both append a trailing newline automatically. Ruby's print matches Perl's print with no added newline. Ruby additionally has p, which calls .inspect on its argument to show the type and structure of a value — invaluable for debugging and something Perl programmers usually reach for Data::Dumper to get.
Variables & Sigils
No sigils — one kind of name
use v5.38; my $name = "Alice"; my @colors = ("red", "green", "blue"); my %person = (name => "Alice", age => 30); say $name; say "@colors"; say $person{name};
name = "Alice" colors = ["red", "green", "blue"] person = { name: "Alice", age: 30 } puts name puts colors.join(", ") puts person[:name]
Ruby has no sigils at all: a scalar, an array, and a hash are all referenced with a bare, unpunctuated name — the type is determined by what the variable holds, not by a leading $, @, or %. This eliminates an entire category of Perl syntax rules (element access uses $ even on an @array, interpolation context changes the sigil, and so on): in Ruby the name never changes no matter how the value is used.
my declaration vs bare assignment
use v5.38; my $count = 0; { my $count = 10; # shadows the outer $count in this block say $count; } say $count;
count = 0 if true count = 10 # reassigns the SAME variable — no new scope here puts count end puts count
Perl's my creates a genuinely new lexical variable scoped to the enclosing block, so an inner my $count shadows an outer one. Ruby has no declaration keyword for local variables — a bare assignment either creates or reassigns a variable in the current scope, and if/while blocks do not introduce a new scope the way a Perl { } block does. Only def, class, module, and blocks (do...end/{ } passed to a method) create new scopes in Ruby.
Dynamic typing in both languages
use v5.38; my $value = 42; say ref(\$value) ? "reference" : "plain scalar"; $value = "hello"; # scalars hold any single value freely $value = [1, 2, 3]; say ref($value); # "ARRAY" — now it's an array reference
value = 42 puts value.class # Integer value = "hello" puts value.class # String value = [1, 2, 3] puts value.class # Array
Both languages let a variable hold a value of any type at any time — neither requires a type declaration. The difference is uniformity: in Perl, a scalar variable can hold a number, a string, or a reference, but an @array variable can never hold a hash without going through a reference. In Ruby, one bare name can be reassigned freely between an integer, a string, or an array, because there is only one kind of variable.
Constants
use v5.38; use constant MAX_RETRIES => 3; use constant API_URL => "https://example.com"; say MAX_RETRIES; say API_URL;
MAX_RETRIES = 3 API_URL = "https://example.com" puts MAX_RETRIES puts API_URL
Ruby constants are simply names that start with an uppercase letter — SCREAMING_SNAKE_CASE is the convention for simple values, exactly like Perl's use constant. Unlike Perl's pragma, Ruby needs no special declaration form: a plain assignment to an uppercase name is enough, and the runtime enforces the convention by emitting a warning (not an error) if the constant is later reassigned.
Truthiness — the "0" trap disappears
use v5.38; # false: undef, 0, "", "0" # true: everything else — including "00" and " " for my $value (undef, 0, "", "0", "00", " ") { my $label = defined($value) ? "'$value'" : "undef"; say "$label is " . ($value ? "truthy" : "falsy"); }
# Ruby: ONLY nil and false are falsy puts "0 is truthy" if 0 # prints! puts "'' is truthy" if "" # prints! puts "[] is truthy" if [] # prints! puts "'0' is truthy" if "0" # prints! (unlike Perl) puts "nil is falsy" unless nil puts "false is falsy" unless false
Ruby's truthiness is far simpler than Perl's: exactly two values are falsy, nil and false. Everything else is truthy — including 0, "", and critically the string "0", which is famously falsy in Perl. This "0 but true" trap that catches Perl programmers checking a string result does not exist in Ruby at all; a non-empty string is always truthy regardless of its content.
Multiple assignment / destructuring
use v5.38; my ($first, $second, $third) = (1, 2, 3); say "$first $second $third"; # Swap without a temp variable ($first, $second) = ($second, $first); say "$first $second"; # Destructure a list my ($head, @tail) = (10, 20, 30, 40); say $head; say "@tail";
first, second, third = 1, 2, 3 puts "#{first} #{second} #{third}" # Swap without a temp variable first, second = second, first puts "#{first} #{second}" # Destructure an array head, *tail = [10, 20, 30, 40] puts head puts tail.inspect
Both languages support list-style destructuring assignment and the classic temp-free swap idiom — Ruby inherited this pattern directly from Perl. Ruby's splat *tail plays the same role as Perl's @tail soaking up the remaining elements, but Ruby needs no explicit parentheses around the left-hand side and no sigils to distinguish which name captures the rest.
Strings
String interpolation
use v5.38; my $name = "Alice"; my $age = 30; say "Hello, $name! You are $age years old."; say "Next year: @{[ $age + 1 ]}"; # expression interpolation trick
name = "Alice" age = 30 puts "Hello, #{name}! You are #{age} years old." # Any expression works inside #{} puts "Next year: #{age + 1}"
Perl interpolates a bare $name or @array directly inside a double-quoted string, but embedding an arbitrary expression like age + 1 requires the awkward @{[ expression ]} trick. Ruby's #{expression} syntax accepts any expression directly — method calls, arithmetic, ternaries — with no special-casing needed, because interpolation always evaluates Ruby code rather than only substituting a variable's value.
String methods as method calls
use v5.38; my $text = " Hello, World! "; $text =~ s/^\s+|\s+$//g; # trim: no built-in method, use regex say $text; say length($text); say uc($text); say lc($text); say reverse($text); say index($text, "World") >= 0 ? "found" : "not found";
text = " Hello, World! " puts text.strip puts text.strip.length puts text.strip.upcase puts text.strip.downcase puts text.strip.reverse puts text.strip.include?("World")
Perl's string operations are mostly standalone functions — length($text), uc($text), index($text, ...) — that take the string as an argument, and there is no built-in trim function at all (trimming needs a manual regex substitution). Ruby's String class has over 80 built-in methods called directly on the string object: text.strip, text.upcase, text.include?. The method-call style reads left-to-right and chains naturally.
Heredocs
use v5.38; my $name = "Alice"; my $message = <<~LETTER; Dear $name, Welcome to the team! Regards, HR LETTER print $message;
name = "Alice" message = <<~LETTER Dear #{name}, Welcome to the team! Regards, HR LETTER puts message.chomp
Ruby's squiggly heredoc syntax (<<~LABEL) is essentially identical to Perl's — both strip the common leading indentation so the source code can be indented naturally without that indentation leaking into the string. Both interpolate variables and expressions by default; a single-quoted terminator (<<~'LABEL' in Ruby, <<~'LABEL' in Perl) disables interpolation in either language.
sprintf-style formatting
use v5.38; printf("%-10s: %6.2f\n", "Price", 9.99); say sprintf("Hex: 0x%08X", 255); say sprintf("Count: %05d", 42);
puts "%-10s: %6.2f" % ["Price", 9.99] puts "Hex: 0x%08X" % 255 puts "Count: %05d" % 42
Ruby's % operator applies the same printf-style format specifiers Perl's sprintf and printf use — both languages inherited this format-string syntax from C. The single-argument form takes the value directly ("%05d" % 42); pass an array for multiple substitutions, exactly as Perl's sprintf takes a list of arguments after the format string.
Frozen strings (Ruby 4.0) vs mutable Perl scalars
use v5.38; # Perl scalars holding strings are always mutable my $greeting = "hello"; $greeting .= ", world"; # append in place, no error say $greeting;
# In Ruby 4.0, string literals are frozen (immutable) by default greeting = "hello" puts greeting.frozen? # true begin greeting << ", world" # attempt to mutate rescue FrozenError => error puts error.message end mutable = greeting.dup # dup creates an unfrozen copy mutable << ", world" puts mutable
Perl string scalars are always mutable — .= appends in place with no restriction. Ruby 4.0 freezes string literals by default, so an in-place mutation like << raises FrozenError unless the string was explicitly duplicated first with dup or the unary plus operator (+greeting). This is a deliberate safety feature with no Perl equivalent: it catches accidental mutation of a value another part of the program still expects to be constant.
Numbers
One numeric family vs Perl's single scalar
use v5.38; my $integer = 42; my $float = 3.14; say ref(\$integer); # "SCALAR" — Perl doesn't distinguish int from float at the type level say $integer + $float; say 2**64; # Perl promotes to floating point beyond native integer range
integer = 42 float = 3.14 puts integer.class # Integer puts float.class # Float puts integer + float puts 2**64 # exact — arbitrary precision, never promotes to float
Perl has one scalar type for all numbers — integers and floats are the same underlying representation, and very large integers silently become imprecise floating-point approximations once they exceed the native integer range. Ruby has a real Integer class with arbitrary precision: 2**64 is computed exactly, with no float promotion and no precision loss, no matter how large the number grows.
Integer methods vs standalone operators
use v5.38; my $number = 42; say $number % 2 == 0 ? "even" : "odd"; say $number > 0 ? "positive" : "not positive"; say abs(-42); for my $count (0..2) { say "tick"; }
number = 42 puts number.even? # true puts number.odd? # false puts number.positive? # true puts number.abs # 42 3.times { puts "tick" }
In Perl, checking parity or sign is an inline expression using % and comparison operators, and abs is a standalone function. Ruby integers are objects with predicate methods — even?, odd?, positive?, zero? — called directly on the number. 3.times { } replaces the for my $count (0..2) counting loop as a self-contained method call on the integer itself.
Division and modulo with negative numbers
use v5.38; say 10 / 3; # 3.33333... — Perl's / is always floating-point division say int(10 / 3); # 3 — truncate toward zero for integer division say 10 % 3; # 1 say -7 % 3; # 2 in Perl — result takes the sign of the right operand
puts 10 / 3 # integer division: 3 (both operands are Integer) puts 10 % 3 # remainder: 1 puts 10.0 / 3 # float division: 3.333... puts -7 % 3 # 2 — same sign convention as Perl
Perl's / always performs floating-point division regardless of operand types — getting an integer result requires int(...) to truncate. Ruby's / performs integer division automatically when both operands are integers, and floor division at that (rounds toward negative infinity, not toward zero). Both languages agree that % returns a result with the same sign as the right operand, so -7 % 3 is 2 in both.
Numeric conversions
use v5.38; my $parsed = "42" + 0; # Perl coerces strings to numbers automatically say $parsed; say "$parsed"; # back to string via interpolation say "3.14" + 0.0; say hex("FF"); # 255 say sprintf("%x", 255); # "ff"
parsed = "42".to_i puts parsed puts parsed.to_s puts parsed.to_f puts "3.14".to_f puts "0xFF".to_i(16) # hex parsing: 255 puts 255.to_s(16) # to hex string: "ff"
Perl coerces strings to numbers implicitly in numeric context — "42" + 0 just works, with warnings if the string is not numeric-looking. Ruby requires explicit conversion methods: to_i, to_f, to_s. This is more typing but removes the ambiguity of Perl's automatic coercion, where a typo like "42abc" + 0 silently produces 42 with a warning rather than an error.
Arrays
Arrays — @array becomes a bare name
use v5.38; my @numbers = (1, 2, 3, 4, 5); my @fruits = ("apple", "banana", "cherry"); say $numbers[0]; # element access uses $, not @ say scalar(@fruits); # count say "@fruits"; # interpolates space-joined
numbers = [1, 2, 3, 4, 5] fruits = ["apple", "banana", "cherry"] puts numbers[0] puts fruits.length puts fruits.join(", ")
Ruby arrays are heap-allocated, dynamically sized, and can hold mixed types, just like Perl arrays. The mechanical difference is sigil consistency: Perl declares an array with @numbers but reads a single element with $numbers[0] (a common source of confusion for beginners), while Ruby uses the same bare name numbers everywhere regardless of whether you are addressing the whole array or one element.
Negative indexing and slicing
use v5.38; my @numbers = (10, 20, 30, 40, 50); say $numbers[0]; # first: 10 say $numbers[-1]; # last: 50 say $numbers[-2]; # second-to-last: 40 my @middle = @numbers[1..3]; say "@middle";
numbers = [10, 20, 30, 40, 50] puts numbers[0] # first: 10 puts numbers[-1] # last: 50 puts numbers[-2] # second-to-last: 40 puts numbers[1, 3].inspect # start, length: [20, 30, 40] puts numbers[1..3].inspect # inclusive range: [20, 30, 40]
Both languages support negative indexing from the end of the array and range-based slicing — Perl's array-slice syntax @numbers[1..3] maps directly onto Ruby's numbers[1..3]. Ruby additionally offers the start, length form (numbers[1, 3]) as an alternative to a range, and slicing an out-of-bounds range returns nil or an empty array rather than raising an error in either language.
push, pop, shift, unshift
use v5.38; my @items = (2, 3); push @items, 4; # add to end unshift @items, 1; # add to front say "@items"; # 1 2 3 4 my $last = pop @items; # remove from end my $first = shift @items; # remove from front say $last; say $first; say "@items";
items = [2, 3] items.push(4) # add to end — also: items << 4 items.unshift(1) # add to front puts items.inspect # [1, 2, 3, 4] last = items.pop # remove from end first = items.shift # remove from front puts last puts first puts items.inspect # [2, 3]
Ruby's push, pop, shift, and unshift are named identically to Perl's built-in functions and behave the same way. The syntactic difference is that Perl's versions are standalone functions taking the array as a parameter (push @items, 4), while Ruby's are methods called on the array object itself (items.push(4)) — the same shift toward method-call syntax seen throughout string and array handling.
map and grep vs map, select, reject
use v5.38; my @numbers = (1, 2, 3, 4, 5, 6); my @doubled = map { $_ * 2 } @numbers; my @evens = grep { $_ % 2 == 0 } @numbers; my @odds = grep { $_ % 2 != 0 } @numbers; say "@doubled"; say "@evens"; say "@odds";
numbers = [1, 2, 3, 4, 5, 6] doubled = numbers.map { |number| number * 2 } evens = numbers.select { |number| number.even? } odds = numbers.reject { |number| number.even? } puts doubled.inspect puts evens.inspect puts odds.inspect
Perl's map and grep are Ruby's map and select almost one-to-one — both transform and filter a list with a block. Ruby adds reject as the inverse of select (Perl programmers negate the grep condition instead), and Ruby's block parameter is named explicitly (|number|) rather than relying on Perl's implicit $_ topic variable.
List::Util reduce vs built-in reduce
use v5.38; use List::Util qw(reduce sum0); my @numbers = (1, 2, 3, 4, 5); my $total = sum0(@numbers); my $product = reduce { $a * $b } @numbers; say $total; say $product;
numbers = [1, 2, 3, 4, 5] total = numbers.reduce(0) { |sum, number| sum + number } product = numbers.inject(:*) # symbol shorthand for the * method puts total # 15 puts product # 120 puts numbers.sum # built-in shorthand
Perl needs the CPAN-distributed core module List::Util to get reduce, sum0, max, and similar aggregate functions — these are not built into arrays themselves and use the implicit $a/$b variables rather than named block parameters. Ruby's Enumerable module gives every collection reduce (alias inject) and sum for free, with a named accumulator parameter and no import required.
sort, uniq, and nested flattening
use v5.38; use List::Util qw(uniq); my @numbers = (3, 1, 4, 1, 5, 9); my @sorted = sort { $a <=> $b } @numbers; say "@sorted"; say join(" ", reverse @sorted); say join(" ", uniq(@numbers));
numbers = [3, 1, 4, 1, 5, 9] puts numbers.sort.inspect puts numbers.sort.reverse.inspect puts numbers.uniq.inspect # remove duplicates nested = [[1, 2], [3, [4, 5]]] puts nested.flatten.inspect # recursively unwrap puts [1, nil, 2, nil, 3].compact.inspect # remove nil values
Perl's sort defaults to string comparison — sorting numbers correctly requires the explicit { $a <=> $b } comparator block, another common beginner trap. Ruby's sort uses the <=> spaceship operator automatically based on the element type, so numeric arrays sort correctly with no comparator needed. Ruby's flatten (recursively unwrapping nested arrays) and compact (removing nil) have no built-in Perl equivalent at all — Perl arrays cannot nest directly without going through references.
Hashes
Hashes — %hash becomes a bare name
use v5.38; my %scores = ( alice => 95, bob => 87, carol => 91, ); say $scores{alice}; # element access uses $, not % say scalar(keys %scores); # count
# Hash literals with symbol keys (most common) scores = { alice: 95, bob: 87, carol: 91 } puts scores[:alice] puts scores.size
Just as with arrays, Perl declares a hash with the % sigil but reads an individual value with $scores{alice}, while Ruby uses one bare name (scores) for the whole structure and every access into it. Ruby's key: syntax (a symbol key) is the idiomatic default and reads closer to Perl's fat-comma key => value style than the older :key => value Ruby syntax it replaced.
Accessing, setting, and checking existence
use v5.38; my %config = (timeout => 30, retries => 3); $config{timeout} = 60; # set / update say $config{timeout}; say exists $config{missing} ? "yes" : "no"; # existence check say $config{missing} // "default"; # defined-or with a default
config = { timeout: 30, retries: 3 } config[:timeout] = 60 # set / update puts config[:timeout] puts config[:missing].inspect # nil — not found, no crash # fetch raises KeyError on missing key begin config.fetch(:missing) rescue KeyError => error puts error.message end puts config.fetch(:missing, "default")
Perl's exists checks for key presence without triggering autovivification, and // (defined-or) supplies a default when a value is undefined. Ruby has no exists keyword — bracket access on a missing key simply returns nil, and fetch is the strict counterpart: fetch(:key) raises KeyError on a missing key, while fetch(:key, default) mirrors Perl's // default pattern.
Iterating over a hash
use v5.38; my %scores = (alice => 95, bob => 87, carol => 91); for my $name (sort keys %scores) { say "$name: $scores{$name}"; } say join(", ", keys %scores); say join(", ", values %scores);
scores = { alice: 95, bob: 87, carol: 91 } scores.each { |name, score| puts "#{name}: #{score}" } puts scores.keys.inspect puts scores.values.inspect puts scores.select { |_name, score| score >= 90 }.inspect
Perl's keys and values functions have direct Ruby method equivalents. Where Perl programmers loop with for my $key (keys %hash) and then look up $hash{$key} inside the loop, Ruby's each yields both the key and value together as block parameters in one step, and — like arrays — hashes include the full Enumerable module, so select, map, and sort_by all work directly on a hash.
Default values and merging
use v5.38; my %word_count; my @words = ("apple", "banana", "apple", "cherry", "apple"); $word_count{$_}++ for @words; # autovivifies each key, starting at undef+1 for my $word (sort keys %word_count) { say "$word: $word_count{$word}"; } my %defaults = (timeout => 30, retries => 3, debug => 0); my %overrides = (timeout => 60); my %merged = (%defaults, %overrides); # later keys win say $merged{timeout};
word_count = Hash.new(0) # default value 0 for missing keys words = ["apple", "banana", "apple", "cherry", "apple"] words.each { |word| word_count[word] += 1 } puts word_count.inspect defaults = { timeout: 30, retries: 3, debug: false } overrides = { timeout: 60 } merged = defaults.merge(overrides) puts merged.inspect
Perl's autovivification lets $word_count{$_}++ spring a missing key into existence and treat undef as 0 for the increment — implicit and occasionally surprising. Ruby makes this explicit with Hash.new(0), which declares the default value for missing keys up front. Merging two hashes with later keys winning works the same way in both languages: Perl's (%defaults, %overrides) list-flattening versus Ruby's explicit merge method.
Context Sensitivity
Scalar vs list context — a Perl-only concept
use v5.38; my @animals = ("cat", "dog", "bird"); my $count = @animals; # scalar context: array evaluates to its length say $count; # 3 my @copy = @animals; # list context: array evaluates to all its elements say "@copy"; my $last_result = (1, 2, 3); # scalar context on a list: LAST element, not count! say $last_result; # 3 — a classic Perl surprise
animals = ["cat", "dog", "bird"] count = animals.length # explicit method call — no ambiguity puts count copy = animals # always the array itself, no context switch puts copy.inspect # There is no equivalent surprise: an array is always an array last_element = [1, 2, 3].last puts last_element
This is the single biggest conceptual difference between the two languages. In Perl, the same expression (@animals, or a parenthesized list) evaluates differently depending on whether it appears in a context that wants one value (scalar context) or many (list context) — assigning an array to a scalar silently gives you its length, but assigning a literal list (1, 2, 3) to a scalar gives you its last element, not its count. Ruby has no context sensitivity at all: an expression always evaluates to exactly one thing, and getting a count always means calling .length or .size explicitly. This eliminates an entire category of Perl surprises, but it also means Ruby cannot express Perl's occasionally-useful "give me count here, list there" idiom without writing it out explicitly.
wantarray — no Ruby equivalent
use v5.38; sub context_aware { if (wantarray()) { return (1, 2, 3); # caller wants a list } else { return "scalar result"; # caller wants a scalar } } my @list_result = context_aware(); my $scalar_result = context_aware(); say "@list_result"; say $scalar_result;
# Ruby methods always return the same value regardless of call site. # There is no equivalent to wantarray — a method cannot inspect how # its return value will be used. def context_aware [1, 2, 3] end list_result = context_aware first_only = context_aware.first puts list_result.inspect puts first_only
Perl's wantarray lets a subroutine inspect the context it was called in and return a different value (or even a different shape of value) depending on whether the caller wants a list, a scalar, or nothing (void context). Ruby has no equivalent mechanism whatsoever — a method always returns the same value, and it is entirely up to the caller to decide what to do with it (call .first, .length, or use the whole thing). This is simpler to reason about but means the same expressive trick Perl uses for dual-purpose subroutines must be written as two differently named methods in Ruby.
Forcing scalar context
use v5.38; my @items = (10, 20, 30, 40); say scalar(@items); # explicit count via the scalar() function say 0 + @items; # numeric context also forces scalar say "There are " . @items . " items"; # string concatenation, too — forces scalar
items = [10, 20, 30, 40] puts items.length # always call the method explicitly puts "There are #{items.length} items" # no implicit context conversion
Perl provides several ways to force scalar context on an array — the explicit scalar() function, or implicitly via numeric or string operators. Ruby has nothing to force, because there is no context to switch: an array is always an array object, and getting its length always means calling .length explicitly, whether inside a string interpolation or anywhere else. This makes Ruby code slightly more verbose in places but removes any ambiguity about what a given expression evaluates to.
Control Flow
if / elsif / else
use v5.38; my $temperature = 22; if ($temperature > 30) { say "hot"; } elsif ($temperature > 20) { say "warm"; } elsif ($temperature > 10) { say "cool"; } else { say "cold"; }
temperature = 22 if temperature > 30 puts "hot" elsif temperature > 20 puts "warm" elsif temperature > 10 puts "cool" else puts "cold" end
Both languages spell the middle branch elsif (not else if). Ruby drops the parentheses around the condition and the curly braces around the body in favor of end — one character shorter than Perl's closing } but requiring the keyword to be spelled out. Ruby's if is also an expression that returns the matched branch's value, so it can be assigned directly: label = if temperature > 20 then "warm" else "cold" end.
unless and postfix modifiers
use v5.38; my $authenticated = 0; unless ($authenticated) { say "Please log in"; } # Postfix form say "Please log in" unless $authenticated; say "Welcome!" if $authenticated;
authenticated = false unless authenticated puts "Please log in" end # Postfix form — identical to Perl's puts "Please log in" unless authenticated puts "Welcome!" if authenticated
Ruby borrowed unless and the postfix statement modifier directly from Perl, and the syntax is essentially identical between the two languages: statement if condition and statement unless condition both read as natural English guards. This is one of the strongest family resemblances between the two languages — a Perl programmer can write postfix conditionals in Ruby with no adjustment at all.
for/foreach vs each and ranges
use v5.38; for my $number (1..5) { say $number * $number; } foreach my $letter ("a".."e") { print "$letter "; } print "\n";
(1..5).each { |number| puts number * number } ("a".."e").each { |letter| print "#{letter} " } puts
Perl's for/foreach over a range and Ruby's .each on a Range object accomplish exactly the same iteration, including support for character ranges like ("a".."e"). The structural difference is that Perl's for is a language keyword with special range syntax, while Ruby's range is a first-class object (1..5) that happens to support each along with the rest of the Enumerable module — map, select, sum, and more, all without a separate loop construct.
while / until
use v5.38; my $counter = 0; while ($counter < 5) { say $counter; $counter++; } $counter = 5; until ($counter == 0) { $counter--; } say $counter;
counter = 0 while counter < 5 puts counter counter += 1 end # until runs while the condition is FALSE counter = 5 until counter == 0 counter -= 1 end puts counter
Both languages have while and until loops with the same semantics — until runs while the condition is false. Neither language has increment/decrement operators natively usable inside Ruby (++ and -- do not exist in Ruby at all — use += 1/-= 1), while Perl retains C-style ++/--. Ruby has no C-style do...while either; use loop do ... break if condition end for guaranteed first execution.
given/when vs case/when
use v5.38; my $direction = "north"; for ($direction) { # topicalize $_ — the "poor man's given/when" since Perl deprecated given/when if (/^north$/) { say "Going up"; last; } if (/^south$/) { say "Going down"; last; } if (/^east$/ || /^west$/) { say "Going sideways"; last; } say "Unknown"; }
direction = "north" case direction when "north" puts "Going up" when "south" puts "Going down" when "east", "west" # comma-separated alternatives puts "Going sideways" when /^n/ # regex match puts "Starts with N" else puts "Unknown" end
Perl once had an experimental given/when construct closely modeled on Ruby's case/when, but it was removed from modern Perl due to design problems with smart matching — the idiomatic replacement is a for ($topic) { if (...) { ...; last } } loop, which is considerably more verbose. Ruby's case/when remains a clean, permanent part of the language: each when clause uses === (case equality) for matching, supporting literal values, regexes, ranges, and comma-separated alternatives in one readable construct with no fallthrough to worry about.
Subroutines & Methods
sub vs def — named parameters vs @_
use v5.38; sub square { my ($number) = @_; # unpack the args array manually return $number * $number; } sub greet { my ($name) = @_; return "Hello, $name!"; } say square(7); say greet("Alice");
def square(number) number * number # last expression is the return value end def greet(name) "Hello, #{name}!" # no return keyword needed end puts square(7) puts greet("Alice")
Perl subroutines receive all arguments bundled into the single array @_ and must manually unpack them with my ($number) = @_; — there is no way to name parameters in the signature itself in the widely-used, portable syntax. Ruby methods declare named parameters directly (def square(number)), so there is no unpacking step. Both languages return the value of the last expression without a return keyword, though Perl programmers use return more consistently than Rubyists do.
Default parameter values
use v5.38; sub greet { my ($name, $greeting) = @_; $greeting //= "Hello"; # defined-or assigns the default return "$greeting, $name!"; } say greet("Alice"); say greet("Bob", "Hi");
def greet(name, greeting = "Hello") "#{greeting}, #{name}!" end puts greet("Alice") puts greet("Bob", "Hi")
Perl has no default-parameter syntax in the subroutine signature — the idiomatic approach is unpacking positionally from @_ and then using //= (defined-or assignment) to fill in a default for any argument left undef. Ruby supports default values directly in the method signature (greeting = "Hello"), evaluated at call time so dynamic defaults like def log(message, time = Time.now) work naturally with no manual assignment step.
Simulated vs real keyword arguments
use v5.38; sub create_user { my (%args) = @_; # unpack into a hash to simulate keyword args my $name = $args{name}; my $age = $args{age}; my $city = $args{city} // "Unknown"; say "$name, age $age, from $city"; } create_user(name => "Alice", age => 30, city => "Paris"); create_user(age => 25, name => "Bob"); # order doesn't matter
def create_user(name:, age:, city: "Unknown") puts "#{name}, age #{age}, from #{city}" end create_user(name: "Alice", age: 30, city: "Paris") create_user(age: 25, name: "Bob") # order doesn't matter
Perl simulates keyword arguments by accepting a flat list of key-value pairs and unpacking it into a hash inside the subroutine (my (%args) = @_;) — there is no way for the signature itself to declare which keywords are required or provide defaults. Ruby has real keyword arguments as a language feature: name: and age: with no default are required and raise ArgumentError if omitted, while city: "Unknown" declares a default directly in the signature — no manual hash unpacking needed.
Variadic arguments — @_ slicing vs *args
use v5.38; sub print_all { my (@items) = @_; say $_ for @items; } sub describe { my (%attributes) = @_; for my $key (sort keys %attributes) { say "$key: $attributes{$key}"; } } print_all(10, 20, 30); describe(name => "Alice", age => 30);
def print_all(*items) items.each { |item| puts item } end def describe(**attributes) attributes.each { |key, value| puts "#{key}: #{value}" } end print_all(10, 20, 30) describe(name: "Alice", age: 30)
Since Perl subroutines already receive every argument in the flat list @_, "variadic" just means unpacking all of it into an array (or a hash, for key-value pairs) rather than a fixed number of named scalars. Ruby makes the intent explicit in the signature: *items collects remaining positional arguments into an array, and **attributes collects keyword arguments into a hash — both are visible in the method definition itself rather than inferred from how @_ happens to be unpacked.
One-liner method syntax (Ruby 3.0+)
use v5.38; sub double_it { my ($number) = @_; return $number * 2; } sub triple_it { my ($number) = @_; return $number * 3; } say double_it(5); say triple_it(5);
def double_it(number) = number * 2 def triple_it(number) = number * 3 puts double_it(5) puts triple_it(5)
Ruby 3.0 introduced an endless method syntax, def name(params) = expression, which reads like a mathematical definition. Perl has no equivalent single-line subroutine form in common use — even a trivial one-expression subroutine needs the full sub name { ... } block with an explicit argument-unpacking line, since there is no parameter list in the sub declaration itself.
Blocks & Closures
Anonymous subs vs blocks
use v5.38; my @names = ("Alice", "Bob", "Carol"); # Anonymous sub passed explicitly as a code reference for my $name (@names) { (sub { say "Hello, $_[0]!" })->($name); } # More idiomatically, just use a for loop directly: say "Hello, $_!" for @names;
names = ["Alice", "Bob", "Carol"] # do...end block for multi-line names.each do |name| puts "Hello, #{name}!" end # { } block for single-line names.each { |name| puts "Hello, #{name}!" }
This is one of Ruby's biggest wins over Perl. Perl has anonymous subroutines (sub { ... }) that must be explicitly created, stored in a scalar, and invoked with an arrow (->()) — there is no lightweight "just pass this code" syntax built into method calls the way iteration methods take a trailing block in Ruby. Ruby's do...end/{ } block is dedicated syntax for passing a chunk of code to a method with no explicit function-object ceremony: names.each do |name| ... end reads naturally and needs no ->() call syntax.
Callback style vs yield
use v5.38; sub repeat { my ($times, $callback) = @_; for my $count (0 .. $times - 1) { $callback->($count); } } repeat(3, sub { my ($count) = @_; say "Step $count"; });
def repeat(times) times.times { |count| yield count } end repeat(3) { |count| puts "Step #{count}" } # block_given? checks whether a block was provided def maybe_transform(value) block_given? ? yield(value) : value end puts maybe_transform(5) { |number| number * 2 } puts maybe_transform(5)
Perl callback-taking subroutines must accept the code reference as an explicit parameter and invoke it with the arrow call syntax ($callback->($count)). Ruby's yield lets any method accept an implicit block with no parameter declared in the signature at all — the block is simply attached at the call site with { ... } or do...end, and block_given? checks whether the caller supplied one. This is the mechanism behind every Ruby iterator: each, map, and select are all implemented with yield.
Code references vs Proc and lambda
use v5.38; my $doubler = sub { my ($number) = @_; return $number * 2; }; my $tripler = sub { my ($number) = @_; return $number * 3; }; say $doubler->(5); say $tripler->(5); # Storing code references in a hash my %operations = ( double => sub { $_[0] * 2 }, square => sub { $_[0] ** 2 }, ); say $operations{double}->(5);
doubler = proc { |number| number * 2 } tripler = lambda { |number| number * 3 } strict = ->(number) { number * 4 } # lambda shorthand puts doubler.call(5) puts tripler.call(5) puts strict.(5) # alternative call syntax # Symbol-to-proc shorthand puts [1, 2, 3].map(&:to_s).inspect
Perl code references (sub { ... } stored in a scalar) are directly analogous to Ruby's Proc objects — both store a block of code as a first-class value that can be passed around and called later with an arrow (->() in Perl, .call or .() in Ruby). Ruby additionally distinguishes lambda from proc: a lambda enforces its argument count strictly and treats return as returning only from the lambda itself, while a plain proc is more forgiving — a distinction Perl code references do not make.
Closures capture lexical scope in both languages
use v5.38; sub make_counter { my $count = 0; return sub { return ++$count; }; } my $counter = make_counter(); say $counter->(); # 1 say $counter->(); # 2 say $counter->(); # 3
def make_counter count = 0 -> { count += 1 } end counter = make_counter puts counter.call # 1 puts counter.call # 2 puts counter.call # 3
Both languages support true lexical closures: an anonymous subroutine (Perl) or lambda (Ruby) captures the enclosing scope's variables by reference, and each call to the factory function creates a fresh, independent closure over its own copy of that state. This is one of the deepest similarities between the two languages — anywhere a Perl programmer reaches for a closure over my variables, the same pattern works in Ruby with -> or proc capturing local variables.
References vs Objects
No explicit references needed
use v5.38; my $name = "Alice"; my $ref = \$name; # backslash creates a reference say $$ref; # $$ dereferences say ref($ref); # "SCALAR" — what type of reference $$ref = "Bob"; # modify via reference say $name; # "Bob" — original is changed
# Ruby objects are always references — no explicit reference syntax name = "Alice" alias_name = name # both variables point to the same String object alias_name.upcase! # mutating through alias_name affects the shared object puts name # "ALICE" — original is changed too
Perl requires an explicit backslash to create a reference and an explicit sigil ($$ref, @$ref, %$ref) to dereference it — references and values are visibly different things. Ruby has no separate reference concept at all: every variable already holds a reference to an object (except immediate values like small integers and symbols), so assigning one variable to another automatically shares the same underlying object, and mutating through either name is visible through both — no \ or $$ ceremony required.
Anonymous refs become plain literals
use v5.38; my $array_ref = [1, 2, 3]; # anonymous array reference my $hash_ref = {name => "Alice", age => 30}; # anonymous hash reference say $array_ref->[1]; say $hash_ref->{name}; push @$array_ref, 4; say "@$array_ref";
array = [1, 2, 3] hash = { name: "Alice", age: 30 } puts array[1] puts hash[:name] array.push(4) puts array.inspect
Perl's [...] and {...} literals create anonymous references that must always be accessed through the arrow operator ($array_ref->[1], $hash_ref->{name}) or an explicit dereferencing sigil (@$array_ref). Ruby's [...] and {...} create real Array and Hash objects directly — the same literal syntax Perl uses for a reference is, in Ruby, simply the value itself, with no arrow and no dereferencing sigil ever needed.
Nested data structures — no dereferencing gymnastics
use v5.38; my @people = ( { name => "Alice", scores => [95, 87, 92] }, { name => "Bob", scores => [78, 83, 90] }, ); for my $person (@people) { my @scores = @{ $person->{scores} }; # dereference the array ref my $total = 0; $total += $_ for @scores; my $average = $total / scalar @scores; say "$person->{name}: $average"; }
people = [ { name: "Alice", scores: [95, 87, 92] }, { name: "Bob", scores: [78, 83, 90] }, ] people.each do |person| average = person[:scores].sum.to_f / person[:scores].size puts "#{person[:name]}: #{average.round(1)}" end
An array of hashes, each holding a nested array, is common in both languages, but accessing it looks very different. Perl requires arrow-chaining through references ($person->{scores}) and an explicit @{ ... } dereference to get back to a plain array before iterating. Ruby's uniform object model needs none of this — person[:scores] already returns the array itself, so .sum and .size can be called directly with no dereferencing step at all.
Regular Expressions
The =~ operator — inherited almost unchanged
use v5.38; my $email = 'user@example.com'; if ($email =~ /\@.*\./) { say "Looks like an email"; } my $text = "The quick brown fox"; if ($text =~ /(\w+)\s+(\w+)$/) { say "Last two words: $1 $2"; }
email = "user@example.com" if email.match?(/\@.*\./) puts "Looks like an email" end text = "The quick brown fox" if (match = text.match(/(\w+)\s+(\w+)$/)) puts "Last two words: #{match[1]} #{match[2]}" end
Ruby inherited the =~ binding operator directly from Perl, along with the regex literal syntax and the numbered capture-group semantics — this is the strongest syntactic family resemblance between the two languages. The main practical difference is how the result is consumed: Perl assigns matched captures straight into the global variables $1, $2, while idiomatic Ruby prefers capturing the MatchData object returned by match and indexing into it (match[1]), though Ruby's $1/$2 globals exist too and work the same way after a successful =~.
s/// substitution vs sub/gsub
use v5.38; my $text = "The cat sat on the mat"; (my $changed = $text) =~ s/cat/dog/; say $changed; my $message = "hello world hello"; (my $all = $message) =~ s/hello/hi/g; # /g = all occurrences say $all;
text = "The cat sat on the mat" changed = text.sub("cat", "dog") puts changed message = "hello world hello" replaced = message.gsub("hello", "hi") puts replaced
Perl's s/pattern/replacement/ modifies a string in place by default and needs the /g flag to replace every occurrence rather than just the first — often paired with the (my $copy = $original) =~ s/... idiom to get a non-destructive copy. Ruby's sub and gsub are separate methods that always return a new string, leaving the original untouched (mirroring Perl's non-/g vs /g distinction as two distinctly named methods); the bang variants sub!/gsub! mutate in place if that behavior is wanted.
Named captures — nearly identical syntax
use v5.38; my $date = "2026-04-22"; if ($date =~ /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/) { say "Year: $+{year}"; say "Month: $+{month}"; say "Day: $+{day}"; }
date = "2026-04-22" if (match = date.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)) puts "Year: #{match[:year]}" puts "Month: #{match[:month]}" puts "Day: #{match[:day]}" end
Named captures use the identical (?<name>...) syntax in both languages — Ruby adopted it directly from Perl, which introduced it in version 5.10. The only difference is how captured names are retrieved afterward: Perl exposes them through the %+ hash ($+{year}), while Ruby exposes them through the MatchData object returned by match (match[:year]), using a symbol key rather than a hash-style variable.
Global match — collecting all occurrences
use v5.38; my $text = 'The price is $10 and $25 and $100'; my @prices = ($text =~ /\$(\d+)/g); # list context collects all matches say join(", ", @prices); my $html = "<b>bold</b> and <i>italic</i>"; while ($html =~ /<(\w+)>/g) { say "Tag: $1"; }
text = 'The price is $10 and $25 and $100' prices = text.scan(/\$(\d+)/).map(&:first) puts prices.join(", ") html = "<b>bold</b> and <i>italic</i>" html.scan(/<(\w+)>/) { |tag| puts "Tag: #{tag[0]}" }
Both patterns for "find every match" exist in Perl and Ruby, but the mechanism differs. Perl relies on context sensitivity: the same =~ /pattern/g expression returns all matches when evaluated in list context, or advances an internal position pointer one match at a time when evaluated in scalar context inside a while loop. Ruby has one dedicated method for this, scan, which always returns every match (or yields each one to a block) with no context-dependent behavior to reason about.
Regex modifiers — same letters, same meaning
use v5.38; my $text = "Hello World"; say "case-insensitive" if $text =~ /hello/i; # /x allows whitespace and comments in the pattern my $date = "2026-04-22"; if ($date =~ / (\d{4}) # year - (\d{2}) # month - (\d{2}) # day /x) { say "Year: $1, Month: $2, Day: $3"; }
text = "Hello World" puts text.match?(/hello/i) # /x is spelled with the "extended" flag Regexp::EXTENDED in Ruby's literal syntax date = "2026-04-22" if (match = date.match(/ (\d{4}) # year - (\d{2}) # month - (\d{2}) # day /x)) puts "Year: #{match[1]}, Month: #{match[2]}, Day: #{match[3]}" end
Ruby uses the exact same modifier letters Perl does, in the exact same position after the closing delimiter: /i for case-insensitive, /m for multiline anchors, /x for extended mode allowing whitespace and comments inside the pattern. This is Perl's regex engine influence at its most direct — a Perl programmer's regex intuition transfers to Ruby almost without translation.
OOP
package + bless vs class
use v5.38; package Animal; sub new { my ($class, %args) = @_; return bless { name => $args{name}, sound => $args{sound} // "...", }, $class; } sub speak { my $self = shift; say "$self->{name} says $self->{sound}"; } package main; my $dog = Animal->new(name => "Rex", sound => "Woof"); $dog->speak;
class Animal def initialize(name:, sound: "...") @name = name @sound = sound end def speak puts "#{@name} says #{@sound}" end end dog = Animal.new(name: "Rex", sound: "Woof") dog.speak
Perl OOP is built manually from two separate primitives: a package to hold the namespace, and bless to associate a plain hash reference with that package, making it recognizable as an object. Ruby bundles all of this into one class keyword — initialize replaces the hand-written new constructor, instance variables (@name) replace the manually managed blessed hash, and self inside a method replaces Perl's convention of my $self = shift; as the first line of every method.
Hand-written accessors vs attr_accessor
use v5.38; package Person; sub new { my ($class, %args) = @_; return bless { name => $args{name}, age => $args{age} }, $class; } sub name { $_[0]->{name} } # read-only getter sub age { # combined getter/setter my $self = shift; $self->{age} = shift if @_; return $self->{age}; } package main; my $person = Person->new(name => "Alice", age => 30); say $person->name; $person->age(31); say $person->age;
class Person attr_reader :name # getter only (read-only) attr_accessor :age # getter + setter def initialize(name, age) @name = name @age = age end end person = Person.new("Alice", 30) puts person.name person.age = 31 # calls the generated age= setter puts person.age
Perl has no built-in accessor generation — every getter and setter is written out by hand, often using the combined pattern shown here ($self->{age} = shift if @_; detects whether the method was called as a setter). Ruby's attr_reader, attr_writer, and attr_accessor generate these methods with a single line each. Ruby setters also use a distinct, more readable call syntax: person.age = 31 instead of Perl's $person->age(31), because age= is a real method Ruby dispatches to when you use assignment syntax on an attribute.
Inheritance — @ISA vs <
use v5.38; package Animal; sub new { bless { name => $_[1] }, $_[0] } sub name { $_[0]->{name} } sub speak { say $_[0]->{name} . " makes a sound" } package Dog; our @ISA = ('Animal'); # declare inheritance sub speak { my $self = shift; say $self->name . " barks"; } package main; my $dog = Dog->new("Rex"); $dog->speak; say $dog->isa("Animal") ? "is an Animal" : "not an Animal";
class Animal def initialize(name) @name = name end def speak "#{@name} makes a sound" end end class Dog < Animal def speak "#{@name} barks" end end animal = Dog.new("Rex") puts animal.speak puts animal.is_a?(Animal)
Perl declares inheritance by populating the special array @ISA with a list of parent package names (or, in modern Perl, the more readable use parent 'Animal'; pragma) — method resolution walks @ISA depth-first to find an inherited method. Ruby uses the < operator directly in the class definition (class Dog < Animal), a single-inheritance declaration built into the syntax rather than assembled from a package variable. Both languages support calling the parent's version of an overridden method — Perl's SUPER::method, Ruby's super.
What Ruby gives you for free
use v5.38; # Perl has no polymorphism check beyond isa/can, no built-in # equality override protocol, and no operator overloading without # the separate "use overload" pragma: package Money; use overload '+' => \&add, '""' => \&stringify; sub new { bless { cents => $_[1] }, $_[0] } sub add { Money->new($_[0]->{cents} + $_[1]->{cents}) } sub stringify { sprintf('$%.2f', $_[0]->{cents} / 100) } package main; my $total = Money->new(150) + Money->new(250); say "$total";
class Money attr_reader :cents def initialize(cents) @cents = cents end def +(other) Money.new(cents + other.cents) end def to_s format("$%.2f", cents / 100.0) end end total = Money.new(150) + Money.new(250) puts total
Operator overloading in Perl requires the separate use overload pragma, mapping each operator symbol to a handler subroutine explicitly. Ruby treats operators as ordinary method names from the start — defining a method literally named + is all that is needed to make + work on custom objects, and to_s (Ruby's built-in stringification hook) plays the same role as Perl's '""' overload without a separate pragma to opt into.
Error Handling
die/eval vs raise/rescue
use v5.38; eval { die "Something went wrong\n"; say "This won't print"; }; if ($@) { say "Caught: $@"; }
begin raise "Something went wrong" rescue => error puts "Caught: #{error.message}" end
die is Perl's raise; a bare eval { } block is the try-block equivalent of Ruby's begin/rescue/end. The key ergonomic difference is how the caught error is exposed: Perl stashes it in the global variable $@ after the eval block ends, which must be checked manually and is easy to forget or accidentally clear with a subsequent eval; Ruby's rescue => error binds the exception object directly to a local variable at the point of the rescue clause, with no global state involved.
warn — identical in both languages
use v5.38; warn "Something looks suspicious\n"; # prints to STDERR say "Execution continues"; my $value = -1; warn "Negative value: $value\n" if $value < 0; say "Processing: $value";
warn "Something looks suspicious" # prints to STDERR puts "Execution continues" value = -1 warn "Negative value: #{value}" if value < 0 puts "Processing: #{value}"
Ruby's warn is functionally identical to Perl's warn — both print a message to STDERR and allow execution to continue, unlike die/raise, which stop the program (or the enclosing eval/begin block). Perl appends a file-and-line-number suffix to a warn message that does not end in a newline; Ruby has no equivalent auto-suffix behavior, so the message prints exactly as given.
or die idiom vs exceptions from the start
use v5.38; sub divide { my ($numerator, $denominator) = @_; die "Cannot divide by zero\n" if $denominator == 0; return $numerator / $denominator; } my $result = eval { divide(10, 2) }; say "10 / 2 = $result" unless $@; eval { divide(10, 0) }; say "Error: $@" if $@;
def divide(numerator, denominator) raise "Cannot divide by zero" if denominator == 0 numerator.to_f / denominator end begin puts divide(10, 2) puts divide(10, 0) rescue => error puts "Error: #{error.message}" end
The ubiquitous Perl idiom open(...) or die "message: $!" exists because many core Perl functions signal failure by returning a false value rather than raising an exception — $! then holds the OS error string (errno). Ruby methods like File.open raise a real exception directly (Errno::ENOENT, for example) rather than returning false, so the or die pattern has no Ruby counterpart: exceptions are the standard way to signal failure throughout Ruby's standard library, not just an option a caller opts into with or die.
Exception objects — bless vs StandardError
use v5.38; package AppError; sub new { my ($class, %args) = @_; return bless { message => $args{message}, code => $args{code} }, $class; } sub message { $_[0]->{message} } sub code { $_[0]->{code} } package main; eval { die AppError->new(message => "Not found", code => 404); }; if (my $error = $@) { if (ref($error) && $error->isa('AppError')) { say "Error " . $error->code . ": " . $error->message; } }
class AppError < StandardError attr_reader :code def initialize(message, code:) super(message) @code = code end end begin raise AppError.new("Not found", code: 404) rescue AppError => error puts "Error #{error.code}: #{error.message}" end
Perl can die with a blessed object instead of a plain string, and the caller must manually check ref($error) && $error->isa('AppError') to determine what was thrown. Ruby has a dedicated exception hierarchy rooted at StandardError/Exception, and rescue AppError => error filters by class directly in the rescue clause syntax — no manual isa check needed, and multiple rescue clauses can catch different exception classes in sequence, trying each in order.
Cleanup — local $@ save/restore vs ensure
use v5.38; sub process_with_cleanup { say "Starting work"; eval { die "failure during work\n"; }; my $error = $@; # save before it can be clobbered say "Cleanup always runs"; die $error if $error; } eval { process_with_cleanup() }; say "Caught: $@" if $@;
def process_with_cleanup puts "Starting work" raise "failure during work" ensure puts "Cleanup always runs" end begin process_with_cleanup rescue => error puts "Caught: #{error.message}" end
Perl has no dedicated "always run this" block keyword — guaranteed cleanup requires either a nested eval with careful manual saving of $@ before it can be overwritten by a subsequent statement, or a module like Scope::Guard. Ruby's ensure clause is a first-class part of begin/rescue/ensure/end (and works the same way appended directly to a def, as shown here): the ensure block always runs, whether the method succeeded, raised, or even used return early, with no manual state juggling required.
File I/O
Writing a file — open/close vs File.write
use v5.38; # Cannot actually run in this sandboxed environment (no filesystem access). open(my $filehandle, '>', 'output.txt') or die "Could not open output.txt: $!"; print $filehandle "Hello, file!\n"; close($filehandle); say "Wrote output.txt";
# Cannot actually run in this sandboxed environment (no filesystem access). File.write("output.txt", "Hello, file!\n") puts "Wrote output.txt"
Perl's three-argument open takes a mode string ('>' for write, '>>' for append, '<' for read) and a lexical filehandle, and requires an explicit close when finished (or relies on the filehandle going out of scope). Ruby's File.write is a one-line convenience method for the common "write this whole string to this path" case; for finer control, File.open(path, "w") { |file| ... } mirrors Perl's explicit-handle style but auto-closes the file when the block ends, with no explicit close call needed. Both examples are marked norun because the sandboxed execution backends for both languages have no real filesystem.
Reading a file — line by line vs slurping
use v5.38; # Cannot actually run in this sandboxed environment (no filesystem access). open(my $filehandle, '<', 'input.txt') or die "Could not open input.txt: $!"; while (my $line = <$filehandle>) { chomp $line; say "Line: $line"; } close($filehandle); # Slurp the whole file at once open(my $slurp_handle, '<', 'input.txt') or die $!; local $/; # undef the input record separator to slurp my $contents = <$slurp_handle>; close($slurp_handle); say length($contents);
# Cannot actually run in this sandboxed environment (no filesystem access). File.foreach("input.txt") do |line| puts "Line: #{line.chomp}" end # Read the whole file at once contents = File.read("input.txt") puts contents.length
Perl reads a file line by line with the diamond operator <$filehandle> inside a while loop, and "slurping" the whole file at once requires the somewhat obscure trick of localizing $/ (the input record separator) to undef. Ruby has two direct, purpose-built methods instead: File.foreach for line-by-line iteration, and File.read to slurp the entire contents into one string — no special-variable manipulation needed for either case.
Special Variables
$_ the default topic vs no implicit topic
use v5.38; my @words = ("apple", "banana", "cherry"); for (@words) { # $_ is implicitly set to each element say uc; # uc() with no argument operates on $_ } say "found" if grep { /an/ } @words; # $_ implicit inside grep's block too
words = ["apple", "banana", "cherry"] words.each do |word| # the block parameter must be named explicitly puts word.upcase end puts "found" if words.any? { |word| word.include?("an") }
Perl's $_ is a pervasive implicit topic variable: many built-in functions (uc, print, chomp) operate on it when called with no argument, and constructs like for, map, and grep set it automatically for each element. Ruby has no equivalent implicit variable — every block explicitly names its parameter (|word|), which is more verbose for quick one-off transformations but means there is never any ambiguity about which value a bare method call is operating on.
@ARGV / %ENV vs ARGV / ENV
use v5.38; # Command-line arguments say "Arguments: @ARGV"; say "First argument: $ARGV[0]" if @ARGV; # Environment variables say $ENV{HOME} // "no HOME set"; $ENV{MY_VAR} = "custom value"; # setting also works say $ENV{MY_VAR};
# Command-line arguments puts "Arguments: #{ARGV.join(' ')}" puts "First argument: #{ARGV[0]}" if ARGV.any? # Environment variables puts ENV["HOME"] || "no HOME set" ENV["MY_VAR"] = "custom value" # setting also works puts ENV["MY_VAR"]
Perl's @ARGV array and %ENV hash have near-identical Ruby counterparts: ARGV (an Array) and ENV (a Hash-like object), both writable in Ruby just as in Perl. The main difference is again the sigil: Perl's special variables carry @ and % prefixes like any other array or hash, while Ruby's equivalents are simply named constants — ARGV and ENV — accessed with ordinary method and index syntax.
Process and script identity
use v5.38; say "Script name: $0"; say "Process ID: $$"; say "Perl version: $]" if 0; # would print numeric version if uncommented
puts "Script name: #{$PROGRAM_NAME}" puts "Process ID: #{Process.pid}" puts "Ruby version: #{RUBY_VERSION}"
Perl exposes process identity through terse punctuation variables: $0 for the script name, $$ for the process ID. Ruby provides the same information through more self-describing names: $PROGRAM_NAME (also aliased as $0 for Perl-familiarity), Process.pid, and the constant RUBY_VERSION — all readable without needing to look up what a bare punctuation mark means.
CPAN vs RubyGems
CPAN vs RubyGems
# Not runnable — package manager commands, not code. # Install a module from CPAN: # cpan install JSON::PP # Or with the modern cpanm: # cpanm JSON::PP # # Use it in a script: use JSON::PP; my $json = encode_json({ name => "Alice", age => 30 }); print "$json\n";
# Not runnable — package manager commands, not code. # Install a gem from RubyGems: # gem install json # Or declare it in a Gemfile and run: # bundle install # # Use it in a script (json is part of Ruby's standard library): require "json" puts JSON.generate({ name: "Alice", age: 30 })
CPAN (the Comprehensive Perl Archive Network) predates RubyGems, npm, and pip by roughly a decade and pioneered the idea of a centralized, versioned package archive with a consistent module-distribution format — a direct ancestor of the modern package-manager landscape. RubyGems plays the same role for Ruby, typically paired with Bundler and a Gemfile to pin exact versions for a project, a workflow CPAN later adopted its own equivalents for (cpanfile, Carton). Both ecosystems are vast, but Ruby's standard library ships more "batteries included" (JSON support, for example, needs no external dependency) where Perl more often reaches for a CPAN module even for common tasks.