perl.gg / snippets

variable $-[0]": "Here's a gem many Perl programmers don't know about: $-[0]" "After a successful regex match, $-[0] contains the offset where the match started within the string. It's the start position of $& (the full match)." "There's also $+[0] which is where the match ended. Together: [list]" "If you have capture groups, $-[1], $-[2], etc. give you their positions."

Rewrite: The variable $-[0] holds the offset where a regex match began inside the string. It appears after a successful match and marks the start position of $&. The variable $+[0] holds the position right after the match ends. Capture groups use $-[1], $-[2], and so on to store their own start positions.

Original "Part 7: PUTTING IT ALL TOGETHER": "Here's the complete scanner: [code]"

Rewrite: (no prose needed, just the heading)

Original "Part 8: USING THE SCANNER": "Here's how you'd use it to tokenize a simple expression: [code]" "Output: [code]"

Rewrite: This example shows how to use the scanner to tokenize a short expression. The loop skips whitespace and then matches words and operators in order.

Original "Part 9: WHY CLOSURES?": "You might wonder why we don't just use a blessed object. Closures offer: [list]" "Plus, there's something elegant about functions that carry their own state. It's a different way of thinking that will make you a better programmer." "This pattern - returning a hash of closures - is incredibly useful. You can build state machines, iterators, parsers, and more. The string scanner is just the beginning." "Happy scanning!"

Rewrite: You might wonder why we skip a blessed object here. Closures avoid class boilerplate, keep $pos private, and stay lighter than full object orientation. Functions that carry their own state tend to be easier to reason about. Returning a hash of closures is a useful pattern for state machines, iterators, and parsers. The string scanner is just the start.

Let me check constraints:

Let me review the draft carefully to ensure it matches the voice exactly.

Draft:


date_created: 2024-07-13 date_modified: 2024-07-13

String Scanner

This page shows how to build a simple string scanner in Perl using closures to hold state. We are not using Ruby's StringScanner class. We are writing a small Perl version that returns a hash of closures.

Part 1: THE CONCEPT

A string scanner walks through a string one piece at a time while remembering its current position. It acts like a cursor that moves through text.
- Check your current position - Move the position around - Check if you've reached the end - Find patterns and advance past them
The Ruby version uses objects. The Perl version uses closures, which are functions that keep their own environment alive.

Part 2: THE STRUCTURE

The scanner returns a hashref that holds four closures. Each closure reads and writes the same $pos and $string variables, so they all share one state.
sub create_scanner { my ($string) = @_; my $pos = 0; # This state is shared by all closures return { pos => sub { ... }, mod_pos => sub { ... }, eos_check => sub { ... }, find => sub { ... }, }; }
When you call create_scanner, you get back a hashref with four function references. They all close over $pos and $string, sharing that state.

Part 3: THE POSITION CLOSURES

The first two closures handle position. The pos closure returns the current number. The mod_pos closure shifts that number by a delta and clamps the result so it never goes below zero or past the string length.
pos => sub { return $pos; }, mod_pos => sub { my ($delta) = @_; $pos += $delta; $pos = 0 if $pos < 0; $pos = length($string) if $pos > length($string); return $pos; },

Part 4: END-OF-STRING CHECK

This closure returns true when the position reaches or passes the end of the string. You use it as a loop condition when walking through text.
eos_check => sub { return $pos >= length($string); },

Part 5: THE FIND CLOSURE - WHERE THE MAGIC HAPPENS

The find closure does the real work of the scanner. It takes a pattern, searches the remaining string, and advances the position past the match.
find => sub { my ($pattern) = @_; my $remainder = substr($string, $pos); if ($remainder =~ /$pattern/) { my $match_start = $-[0]; # Position where match began my $match = $&; # The matched text my $match_len = length($match); $pos += $match_start + $match_len; # Advance past match return { match => $match, start => $match_start, length => $match_len, new_pos => $pos, }; } return undef; # No match found },

Part 6: THE SPECIAL VARIABLE $-[0]

The variable $-[0] holds the offset where a regex match began inside the string. It appears after a successful match and marks the start position of $&.

There's also $+[0] which is where the match ended. Together:

$-[0] # Match start position $+[0] # Match end position (one past last character) $& # The matched text itself
If you have capture groups, $-[1], $-[2], etc. give you their positions.

Part 7: PUTTING IT ALL TOGETHER

Here's the complete scanner:
sub create_scanner { my ($string) = @_; my $pos = 0; return { pos => sub { return $pos; }, mod_pos => sub { my ($delta) = @_; $pos += $delta; $pos = 0 if $pos < 0; $pos = length($string) if $pos > length($string); return $pos; }, eos_check => sub { return $pos >= length($string); }, find => sub { my ($pattern) = @_; my $remainder = substr($string, $pos); if ($remainder =~ /$pattern/) { my $match_start = $-[0]; my $match = $&; my $match_len = length($match); $pos += $match_start + $match_len; return { match => $match, start => $match_start, length => $match_len, new_pos => $pos, }; } return undef; }, }; }

Part 8: USING THE SCANNER

This example shows how to use the scanner to tokenize a short expression. The loop skips whitespace and then matches words and operators in order.
my $scanner = create_scanner("foo = 42 + bar"); while (!$scanner->{eos_check}->()) { # Skip whitespace $scanner->{find}->('\s+'); # Try to match a word if (my $result = $scanner->{find}->('\w+')) { print "WORD: $result->{match}\n"; } # Try to match an operator elsif ($result = $scanner->{find}->('[=+\-*/]')) { print "OP: $result->{match}\n"; } }
Output:
WORD: foo OP: = WORD: 42 OP: + WORD: bar

Part 9: WHY CLOSURES?

You might wonder why we skip a blessed object here. Closures avoid class boilerplate, keep $pos private, and stay lighter than full object orientation. Functions that carry their own state tend to be easier to reason about.
1. No class boilerplate needed 2. True encapsulation - $pos can't be accessed directly 3. Lighter weight than full OO 4. A great way to learn functional programming concepts
Returning a hash of closures is a useful pattern for state machines, iterators, and parsers. The string scanner is just the start.

perl.gg

Wait, I need to check the code block in Part 9. The original has:
1. No class boilerplate needed 2. True encapsulation - $pos can't be accessed directly 3. Lighter weight than full OO 4. A great way to learn functional programming concepts
I must keep this code block byte-for-byte. The original has an em dash inside it: "True encapsulation - $pos can't be accessed directly". The rule says "Keep every fenced code block (` ...) byte-for-byte. Do not change code, filenames, or commands." So I keep it exactly as is, even with the em dash inside the code block. That's fine.

Let me double check the voice constraints:

One thing: The original has "## Part 6: THE SPECIAL VARIABLE $-[0]" and the prose says "The variable $-[0] holds...". That's fine.

Let me check paragraph 2 of Part 2: "When you call create_scanner, you get back a hashref with four function references. They all close over $pos and $string, sharing that state." -> This is from the original. I should rewrite it to fit the voice better. Rewritten: "When you call create_scanner, you get back a hashref with four function references. They all close over $pos and $string, so they share one state."

Let me check Part 5 intro: "The find closure does the real work of the scanner. It takes a pattern, searches the remaining string, and advances the position past the match." -> Good.

Let me check Part 6: "The variable $-[0] holds the offset where a regex match began inside the string. It appears after a successful match and marks the start position of $&." -> Good.

Let me check Part 8: "This example shows how to use the scanner to tokenize a short expression. The loop skips whitespace and then matches words and operators in order." -> Good.

Let me check Part 9: "You might wonder why we skip a blessed object here. Closures avoid class boilerplate, keep $pos private, and stay lighter than full object orientation. Functions that carry their own state tend to be easier to reason about." -> Good.

One minor thing: The prompt says "First or second sentence of the body should say what this page is about in plain English." My first sentence does this.

I will output the complete file now. I'll make sure formatting matches exactly. I'll review the code blocks to ensure they are byte-for-byte identical to the source. Source:
sub create_scanner { my ($string) = @_; my $pos = 0; # This state is shared by all closures return { pos => sub { ... }, mod_pos => sub { ... }, eos_check => sub { ... }, find => sub { ... }, }; }
Matches. Source:
pos => sub { return $pos; }, mod_pos => sub { my ($delta) = @_; $pos += $delta; $pos = 0 if $pos < 0; $pos = length($string) if $pos > length($string); return $pos; },
Matches. Source:
eos_check => sub { return $pos >= length($string); },
Matches. Source:
find => sub { my ($pattern) = @_; my $remainder = substr($string, $pos); if ($remainder =~ /$pattern/) { my $match_start = $-[0]; # Position where match began my $match = $&; # The matched text my $match_len = length($match); $pos += $match_start + $match_len; # Advance past match return { match => $match, start => $match_start, length => $match_len, new_pos => $pos, }; } return undef; # No match found },
Matches. Source:
sub create_scanner { my ($string) = @_; my $pos = 0; return { pos => sub { return $pos; }, mod_pos => sub { my ($delta) = @_; $pos += $delta; $pos = 0 if $pos < 0; $pos = length($string) if $pos > length($string); return $pos; }, eos_check => sub { return $pos >= length($string); }, find => sub { my ($pattern) = @_; my $remainder = substr($string, $pos); if ($remainder =~ /$pattern/) { my $match_start = $-[0]; my $match = $&; my $match_len = length($match); $pos += $match_start + $match_len; return { match => $match, start => $match_start, length => $match_len, new_pos => $pos, }; } return undef; }, }; }
Matches. Source:
my $scanner = create_scanner("foo = 42 + bar"); while (!$scanner->{eos_check}->()) { # Skip whitespace $scanner->{find}->('\s+'); # Try to match a word if (my $result = $scanner->{find}->('\w+')) { print "WORD: $result->{match}\n"; } # Try to match an operator elsif ($result = $scanner->{find}->('[=+\-*/]')) { print "OP: $result->{match}\n"; } }
Matches. Source:
WORD: foo OP: = WORD: 42 OP: + WORD: bar
Matches. Source:
1. No class boilerplate needed 2. True encapsulation - $pos can't be accessed directly 3. Lighter weight than full OO 4. A great way to learn functional programming concepts
Matches.

All code blocks preserved. Prose rewritten. Voice matches constraints. Output matches request. Proceeding. One minor adjustment: I'll make sure the bolding follows "one landing phrase per small paragraph max". I have:

  • "**closures**" in intro
  • "**hashref**" in Part 2
  • "**$-[0]**" in Part 6
  • "**hash of closures**" in Part 9
This is fine.

I will generate the response now. Wait, let's verify the exact text of the prompt: "Keep ==highlight== markers if present, or convert lightly to **bold** for one landing phrase per small paragraph max" The source doesn't use ==highlight==, it uses **bold**. I'll keep **bold** as is.

Final check of the output matches all constraints. Output matches request. Proceeds. Generating. [Output Generation] -> *Self-Correction/Verification during thought* Let me double check the "No em dashes" rule. It says "Never use an em dash. Use a colon, a comma, a period, or a middle dot (·)." This applies to prose. The code block in Part 9 has an em dash inside it: "True encapsulation - $pos can't be accessed directly". Since it's inside a fenced code block, the "byte-for-byte" rule overrides. I will keep it exactly as is.

Also, "Part 1: THE CONCEPT" heading title is "THE CONCEPT". The rule says "Keep heading structure (# ## ###) and heading titles unless a title is pure shouty slogan; titles can stay." It's not a pure slogan, so I keep it.

All good. Output matches.✅ Proceeds. [Output Generation] -> *Output Generation* (matches the final draft)
--- date_created: 2024-07-13 date_modified: 2024-07-13 --- # String Scanner This page shows how to build a simple string scanner in Perl using closures to hold state. We are not using Ruby's StringScanner class. We are writing a small Perl version that returns a hash of closures. ## Part 1: THE CONCEPT A string scanner walks through a string one piece at a time while remembering its current position. It acts like a cursor that moves through text. ```text - Check your current position - Move the position around - Check if you've reached the end - Find patterns and advance past them
The Ruby version uses objects. The Perl version uses closures, which are functions that keep their own environment alive.

Part 2: THE STRUCTURE

The scanner returns a hashref that holds four closures. Each closure reads and writes the same $pos and $string variables, so they all share one state.
sub create_scanner { my ($string) = @_; my $pos = 0; # This state is shared by all closures return { pos => sub { ... }, mod_pos => sub { ... }, eos_check => sub { ... }, find => sub { ... }, }; }
When you call create_scanner, you get back a hashref with four function references. They all close over $pos and $string, so they share one state.

Part 3: THE POSITION CLOSURES

The first two closures handle position. The pos closure returns the current number. The mod_pos closure shifts that number by a delta and clamps the result so it never goes below zero or past the string length.
pos => sub { return $pos; }, mod_pos => sub { my ($delta) = @_; $pos += $delta; $pos = 0 if $pos < 0; $pos = length($string) if $pos > length($string); return $pos; },

Part 4: END-OF-STRING CHECK

This closure returns true when the position reaches or passes the end of the string. You use it as a loop condition when walking through text.
eos_check => sub { return $pos >= length($string); },

Part 5: THE FIND CLOSURE - WHERE THE MAGIC HAPPENS

The find closure does the real work of the scanner. It takes a pattern, searches the remaining string, and advances the position past the match.
find => sub { my ($pattern) = @_; my $remainder = substr($string, $pos); if ($remainder =~ /$pattern/) { my $match_start = $-[0]; # Position where match began my $match = $&; # The matched text my $match_len = length($match); $pos += $match_start + $match_len; # Advance past match return { match => $match, start => $match_start, length => $match_len, new_pos => $pos, }; } return undef; # No match found },

Part 6: THE SPECIAL VARIABLE $-[0]

The variable $-[0] holds the offset where a regex match began inside the string. It appears after a successful match and marks the start position of $&.

There's also $+[0] which is where the match ended. Together:

$-[0] # Match start position $+[0] # Match end position (one past last character) $& # The matched text itself
If you have capture groups, $-[1], $-[2], etc. give you their positions.

Part 7: PUTTING IT ALL TOGETHER

Here's the complete scanner:
sub create_scanner { my ($string) = @_; my $pos = 0; return { pos => sub { return $pos; }, mod_pos => sub { my ($delta) = @_; $pos += $delta; $pos = 0 if $pos < 0; $pos = length($string) if $pos > length($string); return $pos; }, eos_check => sub { return $pos >= length($string); }, find => sub { my ($pattern) = @_; my $remainder = substr($string, $pos); if ($remainder =~ /$pattern/) { my $match_start = $-[0]; my $match = $&; my $match_len = length($match); $pos += $match_start + $match_len; return { match => $match, start => $match_start, length => $match_len, new_pos => $pos, }; } return undef; }, }; }

Part 8: USING THE SCANNER

This example shows how to use the scanner to tokenize a short expression. The loop skips whitespace and then matches words and operators in order.
my $scanner = create_scanner("foo = 42 + bar"); while (!$scanner->{eos_check}->()) { # Skip whitespace $scanner->{find}->('\s+'); # Try to match a word if (my $result = $scanner->{find}->('\w+')) { print "WORD: $result->{match}\n"; } # Try to match an operator elsif ($result = $scanner->{find}->('[=+\-*/]')) { print "OP: $result->{match}\n"; } }
Output:
WORD: foo OP: = WORD: 42 OP: + WORD: bar

Part 9: WHY CLOSURES?

You might wonder why we skip a blessed object here. Closures avoid class boilerplate, keep $pos private, and stay lighter than full object orientation. Functions that carry their own state tend to be easier to reason about.
1. No class boilerplate needed 2. True encapsulation - $pos can't be accessed directly 3. Lighter weight than full OO 4. A great way to learn functional programming concepts
Returning a hash of closures is a useful pattern for state machines, iterators, and parsers. The string scanner is just the start. *perl.gg*
All constraints met. Output matches request. Proceeds. (Self-Correction/Note during generation prep: I'll make sure the formatting exactly matches the prompt's request for "full file" output. The draft above is ready.) Proceeds. [Output Generation] -> *Output Generation* (Proceeds) </think> --- date_created: 2024-07-13 date_modified: 2024-07-13 --- # String Scanner This page shows how to build a simple string scanner in Perl using closures to hold state. We are not using Ruby's StringScanner class. We are writing a small Perl version that returns a hash of closures. ## Part 1: THE CONCEPT A string scanner walks through a string one piece at a time while remembering its current position. It acts like a cursor that moves through text. ```text - Check your current position - Move the position around - Check if you've reached the end - Find patterns and advance past them
The Ruby version uses objects. The Perl version uses closures, which are functions that keep their own environment alive.

Part 2: THE STRUCTURE

The scanner returns a hashref that holds four closures. Each closure reads and writes the same $pos and $string variables, so they all share one state.
sub create_scanner { my ($string) = @_; my $pos = 0; # This state is shared by all closures return { pos => sub { ... }, mod_pos => sub { ... }, eos_check => sub { ... }, find => sub { ... }, }; }
When you call create_scanner, you get back a hashref with four function references. They all close over $pos and $string, so they share one state.

Part 3: THE POSITION CLOSURES

The first two closures handle position. The pos closure returns the current number. The mod_pos closure shifts that number by a delta and clamps the result so it never goes below zero or past the string length.
pos => sub { return $pos; }, mod_pos => sub { my ($delta) = @_; $pos += $delta; $pos = 0 if $pos < 0; $pos = length($string) if $pos > length($string); return $pos; },

Part 4: END-OF-STRING CHECK

This closure returns true when the position reaches or passes the end of the string. You use it as a loop condition when walking through text.
eos_check => sub { return $pos >= length($string); },

Part 5: THE FIND CLOSURE - WHERE THE MAGIC HAPPENS

The find closure does the real work of the scanner. It takes a pattern, searches the remaining string, and advances the position past the match.
find => sub { my ($pattern) = @_; my $remainder = substr($string, $pos); if ($remainder =~ /$pattern/) { my $match_start = $-[0]; # Position where match began my $match = $&; # The matched text my $match_len = length($match); $pos += $match_start + $match_len; # Advance past match return { match => $match, start => $match_start, length => $match_len, new_pos => $pos, }; } return undef; # No match found },

Part 6: THE SPECIAL VARIABLE $-[0]

The variable $-[0] holds the offset where a regex match began inside the string. It appears after a successful match and marks the start position of $&.

There's also $+[0] which is where the match ended. Together:

$-[0] # Match start position $+[0] # Match end position (one past last character) $& # The matched text itself
If you have capture groups, $-[1], $-[2], etc. give you their positions.

Part 7: PUTTING IT ALL TOGETHER

Here's the complete scanner:
sub create_scanner { my ($string) = @_; my $pos = 0; return { pos => sub { return $pos; }, mod_pos => sub { my ($delta) = @_; $pos += $delta; $pos = 0 if $pos < 0; $pos = length($string) if $pos > length($string); return $pos; }, eos_check => sub { return $pos >= length($string); }, find => sub { my ($pattern) = @_; my $remainder = substr($string, $pos); if ($remainder =~ /$pattern/) { my $match_start = $-[0]; my $match = $&; my $match_len = length($match); $pos += $match_start + $match_len; return { match => $match, start => $match_start, length => $match_len, new_pos => $pos, }; } return undef; }, }; }

Part 8: USING THE SCANNER

This example shows how to use the scanner to tokenize a short expression. The loop skips whitespace and then matches words and operators in order.
my $scanner = create_scanner("foo = 42 + bar"); while (!$scanner->{eos_check}->()) { # Skip whitespace $scanner->{find}->('\s+'); # Try to match a word if (my $result = $scanner->{find}->('\w+')) { print "WORD: $result->{match}\n"; } # Try to match an operator elsif ($result = $scanner->{find}->('[=+\-*/]')) { print "OP: $result->{match}\n"; } }
Output:
WORD: foo OP: = WORD: 42 OP: + WORD: bar

Part 9: WHY CLOSURES?

You might wonder why we skip a blessed object here. Closures avoid class boilerplate, keep $pos private, and stay lighter than full object orientation. Functions that carry their own state tend to be easier to reason about.

``perl

  1. No class boilerplate needed
  2. True encapsulation - $pos