<!-- category: hidden-gems -->
The List Repetition Operator (x) x N
Build SQL placeholders. One line.Thatmy $placeholders = join ", ", ("?") x scalar @values; # "?, ?, ?, ?, ?"
("?") x 5 is the list repetition operator. Same x that
does string repetition, but in list context it does something
completely different. It replicates a list.
String repetition: "ha" x 3 gives "hahaha".
List repetition: ("ha") x 3 gives ("ha", "ha", "ha").
The parentheses change everything.
Part 1: STRING REPETITION (THE ONE YOU KNOW)
Thex operator in scalar context repeats a string:
This is the familiar version. A string on the left, a count on the right, and you get the string repeated N times. Useful for formatting and padding.my $line = "-" x 40; say $line; # ---------------------------------------- my $laugh = "ha" x 5; say $laugh; # hahahahaha my $indent = " " x 4; say "${indent}indented text"; # indented text
Nothing surprising here. But watch what happens when you add parentheses.
Part 2: LIST REPETITION (THE ONE YOU DON'T)
Wrap the left operand in parentheses andx operates on a list
instead of a string:
The parentheses force list context on the left side.my @five_zeros = (0) x 5; # @five_zeros is (0, 0, 0, 0, 0) my @three_hellos = ("hello") x 3; # @three_hellos is ("hello", "hello", "hello") my @empties = ('') x 10; # @empties is ('', '', '', '', '', '', '', '', '', '')
x sees a list
and replicates the entire list, not a string. The result is a list
with N copies of the original elements.
This isn't some special syntax. It's the same x operator following
Perl's normal context rules. Parentheses create a list. x on a
list replicates it. Perl being Perl.
Part 3: THE CRUCIAL PARENTHESES
Without parentheses,x always does string repetition:
The difference is everything.my @wrong = "?" x 5; # @wrong has ONE element: "?????" # string repetition, then single-element list assignment my @right = ("?") x 5; # @right has FIVE elements: ("?", "?", "?", "?", "?") # list repetition
"?" x 5 is a string operation that
produces "?????". ("?") x 5 is a list operation that produces
five separate "?" strings.
You can prove this to yourself:
When building SQL placeholders or initializing arrays, you almost always want the parenthesized version.my @a = "x" x 3; say scalar @a; # 1 (one element: "xxx") my @b = ("x") x 3; say scalar @b; # 3 (three elements: "x", "x", "x")
Part 4: SQL PLACEHOLDER GENERATION
This is the killer use case. Building parameterized SQL queries with the right number of placeholders:The number of placeholders automatically matches the number of values. Add a value tomy @values = ("Alice", 30, "alice@example.com"); my $placeholders = join ", ", ("?") x scalar @values; my $sql = "INSERT INTO users (name, age, email) VALUES ($placeholders)"; say $sql; # INSERT INTO users (name, age, email) VALUES (?, ?, ?)
@values and the SQL adapts. Remove one and
it shrinks. No counting. No off-by-one errors.
For IN clauses:
Combine with DBI:my @ids = (101, 102, 103, 104); my $in_clause = join ", ", ("?") x scalar @ids; my $sql = "SELECT * FROM orders WHERE id IN ($in_clause)"; # SELECT * FROM orders WHERE id IN (?, ?, ?, ?)
The placeholder count always matches the bind variable count. The list repetition operator guarantees it. Ifmy @ids = (101, 102, 103, 104); my $placeholders = join ", ", ("?") x scalar @ids; my $sth = $dbh->prepare( "SELECT * FROM orders WHERE id IN ($placeholders)" ); $sth->execute(@ids);
@ids has 4 elements,
you get 4 question marks. If it has 400, you get 400. No manual
counting ever.
Part 5: INITIALIZING ARRAYS
Pre-fill an array with a default value:Initialize a 2D grid:my @grid_row = (0) x 80; # 80 zeros my @flags = (undef) x 100; # 100 undefs my @headers = ("TBD") x 12; # 12 placeholder strings
Themy @grid = map { [(0) x 10] } 1 .. 10; # 10x10 grid of zeros, each row is an array reference $grid[5][7] = 1; # set a cell
map creates 10 independent array references. Each one contains
a list of 10 zeros. Without the map, you'd get shared references
(a common trap).
Pre-size an array for performance when you know the final size:
my @buffer = (0) x 1_000_000; # pre-allocate a million-element array
Part 6: CREATING REPEATED PATTERNS
Multi-element lists get replicated too:That's 10 elements. The two-element listmy @pattern = ("red", "blue") x 5; # ("red", "blue", "red", "blue", "red", "blue", # "red", "blue", "red", "blue")
("red", "blue") was
repeated 5 times, producing 10 elements total.
Use this for striped table rows:
Or building test data:my @colors = ("even", "odd") x 50; # alternating even/odd for 100 rows for my $i (0 .. 99) { say "Row $i: $colors[$i]"; }
Careful. That creates 100 references to the same hash. If you modify one, they all change. For independent copies:my @test_data = ( { name => "test", value => 0 }, ) x 100; # 100 identical hash references... wait, that's a trap!
Usemy @test_data = map { { name => "test", value => 0 } } 1 .. 100;
map when you need independent copies of complex structures.
Use x when you need repeated simple values (scalars).
Part 7: HASH INITIALIZATION WITH x
Initialize a hash with default values using a list of keys and repeated values:The hash slicemy @keys = qw(alpha beta gamma delta); my %defaults; @defaults{@keys} = (0) x scalar @keys; # %defaults is (alpha => 0, beta => 0, gamma => 0, delta => 0)
@defaults{@keys} assigns each key a value from the
right-hand list. The list repetition ensures there are enough zeros
for every key.
Or build a "seen" hash from scratch:
A cleaner alternative for the initialization:my @required = qw(name email password); my %seen; @seen{@required} = (0) x @required; # later, mark fields as seen for my $field (@input_fields) { $seen{$field} = 1 if exists $seen{$field}; } # check for missing required fields my @missing = grep { !$seen{$_} } @required;
Both work. Themy %seen = map { $_ => 0 } @required;
map version is arguably more readable. The slice
version with x is more idiomatic old-school Perl.
Part 8: PRACTICAL DATABASE QUERY BUILDING
Putting it all together. A function that builds a parameterized INSERT statement:And a bulk INSERT builder:sub build_insert { my ($table, %data) = @_; my @columns = sort keys %data; my @values = @data{@columns}; my $cols = join ", ", @columns; my $ph = join ", ", ("?") x scalar @columns; my $sql = "INSERT INTO $table ($cols) VALUES ($ph)"; return ($sql, @values); } my ($sql, @bind) = build_insert("users", name => "Alice", email => "alice\@example.com", age => 30, ); say $sql; # INSERT INTO users (age, email, name) VALUES (?, ?, ?) my $sth = $dbh->prepare($sql); $sth->execute(@bind);
List repetition at two levels:sub build_bulk_insert { my ($table, $columns, @rows) = @_; my $cols = join ", ", @$columns; my $row_ph = "(" . join(", ", ("?") x scalar @$columns) . ")"; my $all_ph = join ", ", ($row_ph) x scalar @rows; my $sql = "INSERT INTO $table ($cols) VALUES $all_ph"; my @bind = map { @$_ } @rows; return ($sql, @bind); } my ($sql, @bind) = build_bulk_insert("logs", [qw(timestamp level message)], ["2026-04-28 10:00", "INFO", "Started"], ["2026-04-28 10:01", "ERROR", "Failed"], ["2026-04-28 10:02", "INFO", "Recovered"], ); say $sql; # INSERT INTO logs (timestamp, level, message) # VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?)
("?") x 3 for columns within a row,
then ($row_ph) x 3 for repeated row groups. Clean, correct, and
the counts can never be wrong.
Part 9: COMPARISON TO MAP
map and list repetition overlap for some use cases. When should you
use which?
Use# identical results for simple values my @a = (0) x 5; my @b = map { 0 } 1 .. 5; # both: (0, 0, 0, 0, 0)
x when: you're repeating a simple scalar value. It's
faster and more concise.
Use map when: you need independent copies of references, or
you need to compute each element.
For scalars,# x: all references point to the SAME array my @bad = ([1, 2, 3]) x 5; $bad[0][0] = 99; say $bad[1][0]; # 99 -- oops, same reference! # map: each reference is independent my @good = map { [1, 2, 3] } 1 .. 5; $good[0][0] = 99; say $good[1][0]; # 1 -- independent copy
x is the right tool. For anything involving
references, use map.
Performance-wise, x is faster than map for simple repetition.
The interpreter has a dedicated opcode for it. map has to call a
block for each iteration. For small lists the difference is
negligible. For a million elements, x wins by a meaningful margin.
Part 10: PUTTING IT ALL TOGETHER
#!/usr/bin/env perl use strict; use warnings; use feature 'say'; # String repetition (scalar context) say "-" x 50; # List repetition (list context) my @placeholders = ("?") x 5; say join ", ", @placeholders; # ?, ?, ?, ?, ? # Dynamic SQL my @data = ("Alice", 30, "alice\@example.com"); my $sql = sprintf "INSERT INTO users VALUES (%s)", join ", ", ("?") x scalar @data; say $sql; # Array initialization my @zeros = (0) x 10; say "Zeros: @zeros"; # Pattern repetition my @ab = ("A", "B") x 4; say "Pattern: @ab"; # A B A B A B A B say "-" x 50;
The list repetition operator is one of those features you don't notice until you need it. Then it saves you a loop, a counter, and three lines of code. SQL placeholders are the canonical use case, but array initialization and pattern generation come up constantly.String context: "ha" x 3 -> "hahaha" List context: ("ha") x 3 -> ("ha", "ha", "ha") +------+------+------+ | "ha" | "ha" | "ha" | three separate strings +------+------+------+ vs. +------------------+ | "hahaha" | one string +------------------+ Same operator. Different context. Different result. Classic Perl. .--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
Remember the parentheses. "?" x 5 is a string. ("?") x 5 is a
list. The parentheses are the difference between one thing and five
things.
perl.gg