Ruby to Perl#

You know Ruby. You pass blocks to each and map without thinking, you reach for a Hash by reflex, and arr.select { |x| ... } is muscle memory. This guide maps each of those habits onto Perl, so you can be productive in pperl in an afternoon instead of relearning programming from scratch.

Two shifts land in the first five minutes, so meet them now:

  • Sigils mark the type, not the scope. Ruby uses @var for instance state, $var for globals, and a bare var for locals. Perl uses $x for a single value, @x for an ordered list, %x for a key/value map - the sigil tells you the shape of the data, and it is the same whether the variable is local or global. Section one is about nothing else.

  • Not everything is an object. In Ruby "foo".upcase and [1, 2].sum are method calls because numbers, strings, and arrays are all objects. Perl is pragmatic: a scalar is not an object, there is no universal base class, and those operations are plain functions - uc "foo", sum @a. You call methods only on things you blessed into a class.

Every example here runs on pperl, a Rust reimplementation of Perl 5.42. Each Perl snippet was executed on pperl; the # ... comments show its real output. Start your scripts with:

use v5.42;

That one line turns on strict (no accidental globals), warnings, the say built-in (a print with a trailing newline, like Ruby’s puts), subroutine signatures, and the modern feature set used throughout this guide.

How to read this#

Variables and sigils#

In Ruby a sigil tells you scope: @x is an instance variable, $x a global, x a local. In Perl the sigil tells you shape: $ for a single value, @ for an ordered list, % for a key/value map. my declares a lexically scoped variable, the Perl equivalent of an ordinary local binding - it is how you get a local in Perl, where Ruby just uses a bare name.

name  = "Ada"
langs = ["Ruby", "Perl"]
ages  = {"Ada" => 36, "Yukihiro" => 60}
use v5.42;
my $name  = "Ada";
my @langs = ("Ruby", "Perl");
my %ages  = (Ada => 36, Yukihiro => 60);
say $name;            # Ada
say $langs[0];        # Ruby
say $ages{Ada};       # 36

The gotcha: when you reach into an aggregate for one element, the sigil follows the value you get back, not the container. A single element of @langs is a scalar, so it is $langs[0], not @langs[0]; one value from %ages is $ages{Ada}. The @/% form is the whole collection; the $ form is one piece of it. (And forget the Ruby mapping: a Perl $x is never “a global” the way Ruby’s $x is - the $ here just means “one value.”)

Strings#

Double quotes interpolate variables, single quotes do not, exactly like Ruby’s "..." versus '...'. The Ruby #{expr} form has a direct counterpart for plain variables and a slightly longer one for arbitrary expressions. Concatenation is ., not +.

name     = "Ada"
greeting = "Hello, #{name}"
total    = "Total: #{6 * 7}"
shout    = "AB" * 3
use v5.42;
my $name = "Ada";
my $greeting = "Hello, $name";     # Hello, Ada
my $literal  = 'Hello, $name';     # Hello, $name  (no interpolation)
my $total    = "Total: @{[ 6 * 7 ]}";  # Total: 42
my $joined   = "AB" . "CD";        # ABCD
my $shout    = "AB" x 3;           # ABABAB

The gotcha: a bare "$name" interpolates a variable just like Ruby’s #{name}, but to interpolate an expression you wrap it in the @{[ ... ]} form (it builds a throwaway array and inlines it). String repetition is x, not *, and the join operator is ., not +; + on strings coerces them to numbers and warns. Array elements and hash values interpolate inside double quotes too: "First: $langs[0]" works as written.

Numbers#

Perl has one numeric type at the surface; you do not distinguish Integer from Float. Division always produces a float, like Ruby’s Float division when either operand is a float.

puts 7.0 / 2        # 3.5
puts 7 / 2          # 3   (integer division in Ruby)
puts 2 ** 10        # 1024
puts 10 % 3         # 1
use v5.42;
say 7 / 2;          # 3.5   (always float)
say int(7 / 2);     # 3
say 2 ** 10;        # 1024
say 10 % 3;         # 1

The gotcha: comparison and equality operators come in two families, and you pick by the operands’ type, not the variables’. Numbers use ==, !=, <, >, <=>; strings use eq, ne, lt, gt, cmp. This has no Ruby analogue - Ruby’s == dispatches on the object’s class, while Perl’s == always means numeric equality. "10" == "10.0" is true (numeric), but "10" eq "10.0" is false (string). Using == on strings, or eq on numbers, is a frequent and quiet bug.

Arrays and lists#

A Perl array is your Ruby Array. The big shift is method calls becoming functions: Ruby’s arr.push(x), arr.pop, arr.length become push @arr, x, pop @arr, scalar @arr - the array is an argument, not a receiver.

nums = [1, 2, 3, 4, 5]
nums.push(6)
last = nums.pop          # 6
puts nums.length         # 5
puts nums[1, 2].join(" ")  # 2 3
use v5.42;
my @nums = (1, 2, 3, 4, 5);
push @nums, 6;
my $last = pop @nums;       # 6
say scalar @nums;           # 5
say "@nums[1,2]";           # 2 3   (a slice)

Ruby’s block-taking iterators are the richest part of this mapping. map, select, and sort_by become Perl map, grep, and sort, which take a { ... } block exactly the way Ruby methods do. Inside the block, $_ is the current element - Perl’s implicit block variable, the role Ruby’s |x| parameter plays.

squares = nums.map    { |x| x * x }
evens   = nums.select { |x| x.even? }
desc    = nums.sort   { |a, b| b <=> a }
use v5.42;
my @nums    = (1, 2, 3, 4, 5);
my @squares = map  { $_ * $_ }    @nums;  # 1 4 9 16 25
my @evens   = grep { $_ % 2 == 0 } @nums; # 2 4
my @desc    = sort { $b <=> $a }  @nums;  # 5 4 3 2 1

The gotcha: sort compares as strings by default, so sort (10, 9, 100) gives 10 100 9. For numbers you must pass a comparator: sort { $a <=> $b } @nums. $a and $b are the two elements being compared (the equivalent of Ruby’s |a, b|), and <=> is the numeric three-way operator - the very same spaceship you know from Ruby (cmp is its string sibling).

Hashes#

A Perl hash is your Ruby Hash. Declared with %, accessed one value at a time with $h{key}. The fat comma => you already use in Ruby hash literals works here too, and it auto-quotes the bareword on its left.

h = {apple: 3, pear: 5, plum: 2}
puts h[:apple]                       # 3
puts h.keys.sort.join(",")           # apple,pear,plum
puts h.key?(:pear)                   # true
h.delete(:plum)
h.sort.each { |k, v| puts "#{k} => #{v}" }
use v5.42;
my %h = (apple => 3, pear => 5, plum => 2);
say $h{apple};                          # 3
say join ",", sort keys %h;             # apple,pear,plum
say exists $h{pear} ? "yes" : "no";     # yes
delete $h{plum};
for my $k (sort keys %h) {
    say "$k => $h{$k}";
}

The gotcha: Perl has no symbol type. Where Ruby reaches for :apple as a lightweight, interned key, Perl just uses the string "apple" - and the => fat comma auto-quotes it, so apple => 3 and $h{apple} need no quotes. A Ruby symbol and a Ruby string are different objects; in Perl there is only the string, so the :sym versus "sym" distinction simply vanishes. Membership is exists $h{key}, not Ruby’s key?, and a hash has no order - iterate sort keys %h when you want a stable one.

Context#

This is the chapter to read twice. Ruby has no equivalent, and almost every “Perl is weird” story traces back to it.

Every expression in Perl runs in either list context or scalar context, decided by where it appears, not by what it is. The same array behaves differently in each. Assigning an array to a scalar gives its length; assigning it to a list (a parenthesised set of variables) copies elements. Ruby never overloads assignment this way - a = arr always just aliases the array - so this is genuinely new.

use v5.42;
my @list = (10, 20, 30);

my $count = @list;        # scalar context -> 3 (the length)
say $count;               # 3

my ($first) = @list;      # list context -> first element copied
say $first;               # 10

my ($a, $b) = @list;      # 10 and 20
say "$a $b";              # 10 20

The parentheses on the left are the whole difference. my $x = @list asks “how many?” and gets 3. my ($x) = @list asks “give me the elements” and gets 10. The Ruby reflex reads both as “assign the array,” which is exactly the trap.

A subroutine can ask which context it was called in, with wantarray, and return different things accordingly:

use v5.42;
sub items {
    return wantarray ? (1, 2, 3) : "three items";
}
my @got = items();        # list context
my $msg = items();        # scalar context
say "@got";               # 1 2 3
say $msg;                 # three items

Built-ins use this too. scalar @array forces scalar context to get a count; a regex match with /g returns the list of matches in list context and a true/false in scalar context. The classic idiom for “count the matches” forces a list assignment in scalar context with the so-called countof form:

use v5.42;
my $n = () = "a1b2c3" =~ /\d/g;
say $n;                   # 3

The gotcha: there is no single answer to “what does this array do” - it depends on the surrounding expression. When something returns a number you expected to be a list (or vice versa), context is the first suspect. Reach for scalar to force a count and parentheses on the left to force element copying.

Truthiness#

This is the sharpest break from Ruby in the whole guide. In Ruby only nil and false are falsy - 0 and "" are both true. In Perl the false values are a short, specific list: the number 0, the empty string "", the string "0", and undef (Perl’s nil). Everything else is true.

use v5.42;
for my $v (0, 1, "", "0", "00", "0.0", "false") {
    say "'$v' is ", ($v ? "true" : "false");
}

This prints, exactly:

'0' is false
'1' is true
'' is false
'0' is false
'00' is true
'0.0' is true
'false' is true

The gotcha: 0 and "" are false in Perl but true in Ruby - the single most disruptive habit to unlearn. A Ruby guard like if count fires for count == 0; the Perl if ($count) does not. On the other side, "00" and "0.0" are true in Perl even though they look numerically zero, because only the exact string "0" is false. And unlike Ruby, where you might lean on nil checks, Perl distinguishes “false” from “undefined” with defined-or //, which falls back only on undef (not on 0 or ""):

use v5.42;
my %cfg = (host => "localhost");
my $port = $cfg{port} // 8080;   # key absent -> undef -> 8080
say $port;                       # 8080

Control flow#

The keywords are familiar; the spelling differs. elsif is Perl’s elsif (Ruby’s elsif too - one of the few exact matches), and blocks are braced, with no then and no terminating end.

if    n < 0  then sign = "neg"
elsif n == 0 then sign = "zero"
else              sign = "pos"
end

nums.each { |x| puts x }

while running
  step
end
use v5.42;
my $sign;
if    ($n < 0)  { $sign = "neg"  }
elsif ($n == 0) { $sign = "zero" }
else            { $sign = "pos"  }

for my $x (@nums) { say $x }

while ($running) { step() }

Perl’s postfix modifiers are the direct counterpart of Ruby’s trailing puts x if cond and do_it while cond - a simple statement carrying its own condition or loop:

use v5.42;
say $_ for @nums;                 # loop, one statement
say "negative" if $n < 0;         # guarded statement
$sum += $_ for @nums;             # accumulate

And unless/until are the negated forms of if/while, just like Ruby’s unless/until: say "ok" unless $error.

The gotcha: the ternary uses ?/:, not Ruby’s cond ? a : b - which is actually identical, so this one is free. There is no x if c else y expression form; chain ternaries instead: my $sign = $n < 0 ? "neg" : $n == 0 ? "zero" : "pos".

Subroutines#

def becomes sub. Modern Perl has signatures that look much like Ruby’s parameter lists, including defaults. And the good news a Ruby developer wants to hear: the last evaluated expression is the return value, exactly as in Ruby - return is optional.

def greet(name, greeting = "Hello")
  "#{greeting}, #{name}!"
end

def total(*nums)
  nums.sum
end
use v5.42;
sub greet ($name, $greeting = "Hello") {
    return "$greeting, $name!";
}
say greet("Ada");           # Hello, Ada!
say greet("Yukihiro", "Hi"); # Hi, Yukihiro!

sub area ($w, $h) { $w * $h }   # no return needed; last expr wins
say area(3, 4);             # 12

sub total (@nums) {
    my $sum = 0;
    $sum += $_ for @nums;
    return $sum;
}
say total(1, 2, 3, 4);      # 10

Ruby keyword arguments map to a hash parameter, since key => value pairs flatten into a list that a %opts signature gathers up:

use v5.42;
sub connect_to (%opts) {
    return "host=$opts{host} port=$opts{port}";
}
say connect_to(host => "localhost", port => 8080);
# host=localhost port=8080

The gotcha: a sub returns the value of its last expression even without return (the Ruby reflex carries over), but in Perl that return is also context-sensitive (see Context). If you return @list, the caller gets the list in list context but the count in scalar context. Be deliberate about what you return when it might be used either way.

References#

Perl’s arrays and hashes do not nest directly; you nest them through references, a scalar that points at an aggregate. In Ruby every container is already a reference, so nesting is invisible to you there; in Perl you take a reference with \ (or build one with [ ]/{ }) and follow it with ->.

data = {name: "Ada", langs: ["Perl", "Ruby"]}
puts data[:langs][1]      # Ruby
data[:langs].push("Python")
use v5.42;
my $data = { name => "Ada", langs => ["Perl", "Ruby"] };
say $data->{name};                  # Ada
say $data->{langs}[1];              # Ruby
push $data->{langs}->@*, "Python";  # postfix deref to push
say "@{$data->{langs}}";           # Perl Ruby Python

{ ... } builds an anonymous hash reference and [ ... ] an anonymous array reference - the literal building blocks of nested data, matching Ruby’s {} and [] literals. The -> arrow follows a reference one step; between two subscripts it is optional, so $data->{langs}[1] and $data->{langs}->[1] are the same.

The payoff for a Ruby developer is blocks-as-values. A Ruby block, proc, or lambda becomes a Perl anonymous sub: a closure stored in a scalar, called with ->. It closes over its lexicals exactly like a Ruby block:

use v5.42;
my $doubler = sub { map { $_ * 2 } @_ };
say join ",", $doubler->(1, 2, 3);  # 2,4,6

my $factor = 10;
my $scaler = sub ($x) { $x * $factor };  # closes over $factor
say $scaler->(5);                   # 50

The gotcha: a Perl anonymous sub is a value you must store and call explicitly with ->( ) - there is no implicit yield, no block passed invisibly alongside the arguments. When a builtin wants a block (map/grep/sort, or first/reduce from List::Util), you write the { ... } inline; when you want to pass a callable around as data, you make a sub { ... } and call it through its scalar. The postfix deref forms (->@*, ->%*, ->$*) are the modern, readable way to dereference a whole aggregate.

Regular expressions#

Good news: Perl regex will feel like home. Ruby borrowed its regex literal /.../, its =~ operator, and $1/$2 capture variables straight from Perl. The match operator m//, the substitution s///, and named captures in %+ round it out.

s = "2026-06-20"
if s =~ /(\d{4})-(\d{2})-(\d{2})/
  puts "#{$1} #{$2} #{$3}"
end
t = "a,b,c".gsub(",", ";")
parts = "one,two,three".split(",")
use v5.42;
my $s = "2026-06-20";
if ($s =~ /(\d{4})-(\d{2})-(\d{2})/) {
    say "$1 $2 $3";                 # 2026 06 20
}
if ($s =~ /(?<year>\d{4})/) {
    say $+{year};                   # 2026
}
(my $t = "a,b,c") =~ s/,/;/g;       # a;b;c
my @parts = split /,/, "one,two,three";   # one two three

A /g match in list context returns all captures, the direct analogue of Ruby’s String#scan:

use v5.42;
my @nums = "x1y2z3" =~ /(\d)/g;     # 1 2 3
say "@nums";

The gotcha: where Ruby’s gsub returns a new string and leaves the original alone, Perl’s s/// modifies its target in place and returns the number of substitutions. That is why the example copies first with (my $t = "a,b,c") and then substitutes on the copy. To get a new string without touching the original - the gsub behaviour you expect - add the /r flag: my $t = $s =~ s/,/;/gr.

File and stream I/O#

The idiom is the three-argument open with a lexical filehandle, the counterpart of Ruby’s File.open(path, mode) do |f| ... end. The angle-bracket operator <$fh> reads a line, the same role as Ruby’s f.each_line or f.gets.

File.open("/tmp/sample.txt", "w") do |f|
  (1..3).each { |i| f.write("line #{i}\n") }
end

File.open("/tmp/sample.txt") do |f|
  f.each_line { |line| puts "got: #{line.chomp}" }
end
use v5.42;
my $path = "/tmp/sample.txt";

open my $wh, '>', $path or die "cannot write: $!";
print {$wh} "line $_\n" for 1 .. 3;
close $wh;

open my $rh, '<', $path or die "cannot read: $!";
while (my $line = <$rh>) {
    chomp $line;
    say "got: $line";       # got: line 1 ... got: line 3
}
close $rh;

To slurp every line at once, read the handle in list context:

use v5.42;
open my $fh, '<', "/tmp/sample.txt" or die $!;
my @lines = <$fh>;          # all lines, including newlines
say scalar @lines;          # 3

The gotcha: a line read with <$fh> keeps its trailing newline; call chomp to strip it - Ruby’s line.chomp is the same verb, and Perl borrowed the name. And open returns false on failure rather than raising the way Ruby’s File.open does, so the or die "...: $!" idiom is mandatory; $! is the system error message, Perl’s equivalent of the message in Ruby’s Errno exception.

Modules and packages#

require/include becomes use. The namespace separator is ::, which you already know from Ruby constants like Set::Foo; Perl uses it everywhere, for both packages and the names inside them. A module Foo::Bar lives at Foo/Bar.pm on the include path @INC (Perl’s $LOAD_PATH). A module’s import list is a selection of names, much like pulling specific methods into scope.

require "set"
nums = [1, 2, 3, 4].sum
biggest = [3, 7, 2].max
use v5.42;
use List::Util qw(sum0 max first);
say sum0(1 .. 10);                       # 55
say max(3, 7, 2);                        # 7
say first { $_ % 2 == 0 } (1, 3, 4, 5);  # 4

The standard library is large, and much of what you would reach to RubyGems for ships in core or on CPAN, the Perl package archive. List::Util, Scalar::Util, POSIX, File::Spec, and Data::Dumper are the everyday ones.

The gotcha: use runs at compile time, not at the point in the file where it appears, so the order of use lines relative to your code does not matter the way require placement can. Importing is opt-in per name: use List::Util qw(sum0 max) brings exactly sum0 and max into your namespace, nothing else - there is no monkey-patching of built-in types the way Ruby lets you reopen Array.

Objects#

Ruby is everything-is-an-object; Perl is pragmatic, and the difference showed up already in Arrays (functions, not methods). But when you do want objects, Perl 5.42 has a first-class class syntax that will look familiar. Fields are declared with field, methods with method, and $self is available inside every method just as self is in Ruby (you do not list it as a parameter).

class Point
  attr_accessor :x, :y
  def initialize(x: 0, y: 0)
    @x = x
    @y = y
  end
  def to_string
    "(#{@x}, #{@y})"
  end
  def move(dx, dy)
    @x += dx
    @y += dy
    self
  end
end

p = Point.new(x: 3, y: 4)
puts p.to_string           # (3, 4)
p.move(1, 1)
puts p.to_string           # (4, 5)
use v5.42;
use feature 'class';

class Point {
    field $x :param = 0;
    field $y :param = 0;
    method to_string { return "($x, $y)" }
    method move ($dx, $dy) {
        $x += $dx;
        $y += $dy;
        return $self;
    }
}

my $p = Point->new(x => 3, y => 4);
say $p->to_string;          # (3, 4)
$p->move(1, 1);
say $p->to_string;          # (4, 5)

The constructor is generated for you, the equivalent of Ruby’s attr_accessor plus initialize collapsed into the field declarations: :param marks a field that the auto-built new accepts as a named argument, and = 0 is its default. Inheritance is :isa, the counterpart of Ruby’s class Point3D < Point:

use v5.42;
use feature 'class';

class Point3D :isa(Point) {
    field $z :param = 0;
    method z { return $z }
}

my $q = Point3D->new(x => 1, y => 2, z => 3);
say $q->to_string, " z=", $q->z;   # (1, 2) z=3

The gotcha: the class feature is still marked experimental in Perl 5.42, and on pperl the “class is experimental” notices print to stderr even with no warnings 'experimental::class' in scope. They do not touch your program’s output, so a script that prints to stdout behaves exactly as shown above; just expect the notices on the error stream. Note also that field gives you a :reader for read access but no Ruby-style attr_accessor setter by default - you write a method to mutate, as move does above. For the older bless-based object model you will meet in existing code, and for roles, inheritance details, and migration, see the Object-oriented Programming guide.

Error handling#

Ruby’s begin/rescue/ensure has two counterparts. The classic one wraps code in eval { } and inspects $@ (the caught error) afterward - note that eval here is exception trapping, not Ruby’s eval of a string:

begin
  risky(-1)
rescue => e
  puts "caught: #{e.message}"
end
use v5.42;
my $result = eval { risky(-1) };
if ($@) {
    print "caught: $@";     # caught: negative!
}

The modern form is try/catch/finally, which reads almost exactly like begin/rescue/ensure:

use v5.42;
use feature 'try';

try {
    risky(-4);
}
catch ($e) {
    print "caught: $e";     # caught: negative!
}
finally {
    say "done";             # done   (runs either way, like ensure)
}

where the raising side is die - Perl’s raise:

use v5.42;
sub risky ($n) {
    die "negative!\n" if $n < 0;
    return sqrt $n;
}

The gotcha: die with a string that ends in \n produces exactly that message; die with a string that does not end in a newline gets " at SCRIPT line N.\n" appended automatically. End your error strings with \n when you want a clean message and omit it when you want the file-and-line trace. The finally block, like Ruby’s ensure, runs whether or not an exception was thrown. Note also the two error variables: $@ is the last caught exception (Ruby’s rescue => e), while $! is the last system error (a failed open). Under pperl, the try/catch/finally feature prints an “experimental” notice on stderr; stdout is unaffected.

Idioms#

A few patterns you will see daily and want in your fingers.

Sort a list by a computed key. Ruby has sort_by built in; the Perl hand-rolled form is the Schwartzian transform - decorate, sort, undecorate - and is worth recognising on sight:

use v5.42;
my @words = qw(apple fig pear);
my @by_length =
    map  { $_->[1] }
    sort { $a->[0] <=> $b->[0] }
    map  { [ length $_, $_ ] } @words;
say "@by_length";           # fig pear apple

Build a hash from a list, the Ruby words.to_h { |w| [w, w.length] } analogue:

use v5.42;
my @words = qw(apple pear fig);
my %len = map { $_ => length $_ } @words;
say "$_ => $len{$_}" for sort keys %len;
# apple => 5
# fig => 3
# pear => 4

And pperl is a command-line tool in the awk/sed niche - the role ruby -e and ruby -ne fill for shell glue. The implicit-loop one-liner is the Perl developer’s everyday reflex:

pperl -lane '$s += $F[-1]; END { print $s }' data.txt

The gotcha: read the One-Liners guide for the switches (-n, -p, -a, -l, -e) and the dozens of recipes built on them; it is one of the highest-leverage things to learn coming from a shell-and-Ruby background.

Gotchas coming from Ruby#

The traps, collected for a final scan before you start writing:

  • Sigils mark shape, not scope. $x/@x/%x say single/list/map; they are not Ruby’s instance/global/local markers. One element of @a is $a[0]; one value of %h is $h{k}.

  • Not everything is an object. uc "foo" and sum @a are functions, not methods on a string or array. You call methods only on blessed references.

  • Truthiness is the big one. 0 and "" are false in Perl but true in Ruby. if ($count) is false when $count is 0. Only 0, "", "0", and undef are false; "00" and "0.0" are true.

  • No symbols. :name has no Perl equivalent; hash keys are plain strings, and => auto-quotes the bareword on its left.

  • . joins strings, + adds numbers. Never + two strings to concatenate; that coerces them to numbers and warns. String repeat is x, not *.

  • Two comparison families. ==/</<=> for numbers, eq/lt/ cmp for strings. Pick by operand type, not by variable - unlike Ruby’s class-dispatched ==.

  • sort is string-sorted by default. Pass { $a <=> $b } for numeric order.

  • Context. my $x = @a is the length; my ($x) = @a is the first element. The parentheses are the whole difference. Re-read Context when a value comes back the wrong shape.

  • s/// edits in place and returns a count, unlike Ruby’s gsub; use the /r flag to get a new string instead.

  • open does not raise. Use open ... or die "...: $!".

  • Blocks are not implicit. A stored sub { ... } is called explicitly with ->( ); there is no yield and no invisible block argument.

  • Non-ASCII source needs use utf8;. Under use v5.42; a literal non-ASCII byte in your source is a compile error without it. Keep string data ASCII unless you specifically add use utf8;.

  • The class feature prints experimental notices to stderr on pperl. Correct output, noisy error stream.