Loops With Regex
What if you could iterate over a hash without writing a for loop?Not with while. Not with foreach. With regex substitution.
Output:my $db = { name => 'Mike', age => '45' }; qq|@{[ reverse sort keys %{$db} ]}| =~ s~\S+~print qq|$&: $db->{$&}\n|~ger;
No loop keyword anywhere. Just a string, a regex, and the /e modifier doing things it probably shouldn't.name: Mike age: 45
Part 1: THE NORMAL WAY
Before we go weird, here's how sane people do it:Clean. Readable. Maintainable. Everything your code should be.for my $key (reverse sort keys %{$db}) { print qq|$key: $db->{$key}\n|; }
Now let's throw that out the window.
Part 2: THE DATA
Note: $db is a REFERENCE to a hash, not a hash itself.my $db = { name => 'Mike', age => '45' };
This distinction matters. We'll be dereferencing a lot.$db Hash reference (scalar) %{$db} Dereferenced hash keys %{$db} Keys of the dereferenced hash $db->{name} Value lookup through the reference
Part 3: THE STRING TRICK
This builds a string containing all the hash keys. Let's unpack it:qq|@{[ reverse sort keys %{$db} ]}|
The baby cart @{[...]} is the secret sauce. It:PIECE WHAT IT DOES --------------------------- -------------------------------- qq|...| Double-quoted string @{[ ... ]} The "baby cart" operator keys %{$db} Get all keys from the hash sort Alphabetize them reverse Flip the order
So our string becomes:1. Evaluates the code inside 2. Creates an anonymous array from the result 3. Immediately dereferences it into the string
Just the keys, space-separated. Now we have something to regex against."name age"
Part 4: THE SUBSTITUTION
This is where the loop happens. Breaking it down:=~ s~\S+~print qq|$&: $db->{$&}\n|~ger;
The /g flag is our loop. It finds every match in the string. The /e flag turns each replacement into code execution. For each key matched:PIECE WHAT IT DOES ------- ------------------------------------------------ =~ Bind the string to this regex operation s~~~ Substitution (using ~ as delimiter) \S+ Match one or more non-whitespace chars (a key) print ... The replacement (but it's code, not text) g Global - match ALL keys, not just the first e Evaluate replacement as Perl code r Return result, don't modify original
Two keys, two matches, two print statements. Loop complete.1. $& contains the matched key ("name" or "age") 2. The print statement runs 3. $db->{$&} looks up the value
.--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
Part 5: THE MATCH VARIABLE
This special variable holds whatever the regex just matched.$&
When \S+ matches "name", $& is "name". When \S+ matches "age", $& is "age".
We use it twice in the replacement:
Same variable, two purposes. The key becomes both label and lookup.print qq|$&: $db->{$&}\n| ^^ ^^^^^^^ | | | +-- Look up this key's value +------ Print the key itself
Part 6: WALKTHROUGH
Starting string: "name age"The /g modifier keeps matching until the string is exhausted. Each match triggers the /e evaluation. Two matches, two prints.MATCH $& EXECUTES OUTPUT ----- ------ ----------------------------- ----------- 1 "name" print qq|name: $db->{name}\n| name: Mike 2 "age" print qq|age: $db->{age}\n| age: 45
Part 7: FILTERING WITH GREP
Want only specific keys? Add a grep:The grep filters before the string is built:qq|@{[ grep { m~^name$~ } keys %{$db} ]}| =~ s~\S+~print qq|$&: $db->{$&}\n|~ger;
Now the string is just "name" and we only print one pair.PIECE WHAT IT DOES ----------------------- -------------------------------- keys %{$db} All keys: (name, age) grep { m~^name$~ } Keep only keys matching "name" Result Just: (name)
You can use any condition in the grep:
# Keys starting with 'n' grep { m~^n~ } keys %{$db} # Keys longer than 3 characters grep { length > 3 } keys %{$db} # Keys that aren't 'age' grep { $_ ne 'age' } keys %{$db}
Part 8: THE ANNOTATED VERSION
#!/usr/bin/env perl use feature qw|say|; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Our data structure # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Hash reference (not a hash) my $db = { name => 'Mike', age => '45' }; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # The regex "loop" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Step 1: Build a string of all keys # @{[...]} interpolates code results into the string # reverse sort orders them Z-A # Step 2: Use s///ge to "iterate" # \S+ matches each key # /g makes it match ALL keys # /e evaluates the replacement as code # /r returns result without modifying original # Step 3: $& holds the current match (the key) # Use it to print key and look up value qq|@{[ reverse sort keys %{$db} ]}| =~ s~\S+~print qq|$&: $db->{$&}\n|~ger;
Part 9: WHY YOU SHOULDN'T DO THIS
Let's be clear: this is a terrible idea for production code.Problems:
But it demonstrates:* Unreadable to anyone who hasn't seen this trick * Slower than a real loop (regex overhead) * $& has performance implications (though modern Perl is better) * Breaks if keys contain whitespace * Debugging is a nightmare
These are all useful concepts. Just... use them normally.* The baby cart operator @{[...]} * Regex as iteration with /g * Code execution in substitution with /e * The $& match variable * Reference dereferencing patterns
Part 10: WHEN THIS PATTERN IS USEFUL
The s///ge pattern (minus the weird key iteration) shows up in real code for transformations:The last one is basically a micro-templating engine. Same pattern, legitimate use case.# Convert all numbers to their squares $text =~ s~\d+~$& * $&~ge; # Uppercase all words starting with 'a' $text =~ s~\ba\w+~uc($&)~ge; # Replace placeholders with values $template =~ s~\{\{(\w+)\}\}~$data->{$1}~ge;
The baby cart trick is useful for embedding calculations in strings:
Know the techniques. Use them wisely.say "Sum: @{[ 2 + 2 ]}"; # Sum: 4 say "Time: @{[ scalar localtime ]}"; # Time: Sun Jan 5 14:30:00 2025 say "Files: @{[ `ls | wc -l` ]}"; # Files: 42
perl.gg.---. / o o \ \ L / ----- /| |\ / | | \ | | /| |\ (_| |_) "Who needs for loops anyway?"