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:
- YAML frontmatter kept exactly? Yes.
- Code blocks byte-for-byte? Yes.
- Heading structure kept? Yes.
- Prose rewritten only? Yes.
- First/second sentence says what page is about? Yes, in intro.
- Full sentences, short paragraphs (1-3 sentences)? Yes.
- No em dashes? Checked. I used periods, commas, colons.
- No flyer slogans/stacked fragments? Checked.
- No invented APIs? Checked.
- Bold landing phrase per small paragraph max? I used closures, hashref, $-[0], hash of closures as bold. Let me check if I'm bolding too much. The rule says "One bold landing phrase per small paragraph is still fine. Do not bold a shopping list of nouns." I'll keep one bold per paragraph max.
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.The Ruby version uses objects. The Perl version uses closures, which are functions that keep their own environment alive.- Check your current position - Move the position around - Check if you've reached the end - Find patterns and advance past them
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.When you call create_scanner, you get back a hashref with four function references. They all close over $pos and $string, sharing that 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 { ... }, }; }
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:
If you have capture groups, $-[1], $-[2], etc. give you their positions.$-[0] # Match start position $+[0] # Match end position (one past last character) $& # The matched text itself
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.Output: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"; } }
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.Returning a hash of closures is a useful pattern for state machines, iterators, and parsers. The string scanner is just the start.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
perl.gg
Wait, I need to check the code block in Part 9. The original has: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 (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
` ...) 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:
- "Never use an em dash. Use a colon, a comma, a period, or a middle dot (
·)." -> This applies to prose. Code blocks are exempt.
"First or second sentence of the body should say what this page is about in plain English." -> "This page shows how to build a simple string scanner in Perl using closures to hold state." -> Good.
"Full sentences. Subjects, verbs, articles. Short paragraphs (1-3 sentences)." -> Checked.
"No flyer slogans, stacked fragments, or brochure cadence." -> Checked.
"Do not invent APIs, flags, or steps." -> Checked.
"Keep ==highlight== markers if present, or convert lightly to **bold** for one landing phrase per small paragraph max" -> I used **closures**, **hashref**, **$-[0]**, **hash of closures**. That's fine.
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
- No class boilerplate needed
- True encapsulation - $pos