perl.gg / hidden-gems

<!-- category: hidden-gems -->

The List Repetition Operator (x) x N

2026-04-28

Build SQL placeholders. One line.
my $placeholders = join ", ", ("?") x scalar @values; # "?, ?, ?, ?, ?"
That ("?") 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)

The x operator in scalar context repeats a string:
my $line = "-" x 40; say $line; # ---------------------------------------- my $laugh = "ha" x 5; say $laugh; # hahahahaha my $indent = " " x 4; say "${indent}indented text"; # indented text
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.

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 and x operates on a list instead of a string:
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 ('', '', '', '', '', '', '', '', '', '')
The parentheses force list context on the left side. 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:
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
The difference is everything. "?" 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:

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")
When building SQL placeholders or initializing arrays, you almost always want the parenthesized version.

Part 4: SQL PLACEHOLDER GENERATION

This is the killer use case. Building parameterized SQL queries with the right number of placeholders:
my @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 (?, ?, ?)
The number of placeholders automatically matches the number of values. Add a value to @values and the SQL adapts. Remove one and it shrinks. No counting. No off-by-one errors.

For IN clauses:

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 (?, ?, ?, ?)
Combine with DBI:
my @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);
The placeholder count always matches the bind variable count. The list repetition operator guarantees it. If @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:
my @grid_row = (0) x 80; # 80 zeros my @flags = (undef) x 100; # 100 undefs my @headers = ("TBD") x 12; # 12 placeholder strings
Initialize a 2D grid:
my @grid = map { [(0) x 10] } 1 .. 10; # 10x10 grid of zeros, each row is an array reference $grid[5][7] = 1; # set a cell
The 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:
my @pattern = ("red", "blue") x 5; # ("red", "blue", "red", "blue", "red", "blue", # "red", "blue", "red", "blue")
That's 10 elements. The two-element list ("red", "blue") was repeated 5 times, producing 10 elements total.

Use this for striped table rows:

my @colors = ("even", "odd") x 50; # alternating even/odd for 100 rows for my $i (0 .. 99) { say "Row $i: $colors[$i]"; }
Or building test data:
my @test_data = ( { name => "test", value => 0 }, ) x 100; # 100 identical hash references... wait, that's a trap!
Careful. That creates 100 references to the same hash. If you modify one, they all change. For independent copies:
my @test_data = map { { name => "test", value => 0 } } 1 .. 100;
Use 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:
my @keys = qw(alpha beta gamma delta); my %defaults; @defaults{@keys} = (0) x scalar @keys; # %defaults is (alpha => 0, beta => 0, gamma => 0, delta => 0)
The hash slice @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:

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;
A cleaner alternative for the initialization:
my %seen = map { $_ => 0 } @required;
Both work. The 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:
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);
And a bulk INSERT builder:
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 (?, ?, ?), (?, ?, ?), (?, ?, ?)
List repetition at two levels: ("?") 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?
# identical results for simple values my @a = (0) x 5; my @b = map { 0 } 1 .. 5; # both: (0, 0, 0, 0, 0)
Use 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.

# 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
For scalars, 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;
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 | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
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.

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