gradebook.pl
A single, self-contained program that builds a working class grade book while
introducing every major Perl feature in order. Read it top to bottom; each section
builds on the last. Run it with perl gradebook.pl and it produces
real output plus two files: a text report and a CSV.
A scalar is Perl's basic unit of data. It holds exactly one thing — a number, a string, or a reference. Every scalar variable starts with $. You don't declare a type; Perl figures it out from context.
Always start every Perl file with use strict; use warnings;. These two pragmas turn on the compiler's safety features and catch most beginner mistakes before the program even runs.
#!/usr/bin/env perl use strict; use warnings; use feature 'say'; use List::Util qw(sum max min); my $class_name = "Introduction to Programming"; my $teacher = "Ms. Hernandez"; my $passing_score = 60; my $bonus_points = 2.5; # "" interpolates. '' is always literal. say "Class: $class_name"; say 'Teacher: $teacher'; # prints: $teacher # . concatenates. x repeats. my $separator = "=" x 50; my $title = $class_name . " — " . $teacher; # sprintf formats a string without printing it my $label = sprintf("%-20s %s", "Semester:", "Fall 2025"); say $label; # printf prints a formatted string directly my $weeks = 16; printf "Weeks: %d Hours: %.1f\n", $weeks, $weeks * 1.5;
use strict forces you to declare all variables with my. use warnings warns about things like using undef as a number. Never skip these.
$. Think of it as "give me one thing."
"..." expand variables and \n escapes. Single quotes '...' are completely literal — nothing is expanded.
say is print with a newline automatically appended. It requires use feature 'say'.
. concatenates strings (not +). x repeats a string. ** is exponentiation.
Class: Introduction to Programming Teacher: $teacher Teacher: Ms. Hernandez ================================================== Class: Introduction to Programming — Fall 2025 Semester: Fall 2025 Weeks: 16 Hours: 24.0
An array is an ordered list of scalars. It uses the @ sigil. When you pull one element out, the sigil becomes $ — you're extracting a scalar. Perl arrays grow and shrink dynamically.
my @subjects = ("Math", "English", "Science"); my @scores = (88, 74, 92, 61, 95); my @counts = (1..10); # range → (1,2,3,...,10) # qw() = "quote words" — shorthand for a list of strings my @days = qw(Mon Tue Wed Thu Fri); say join(", ", @subjects); # glue into a string say $subjects[0]; # "Math" — zero-indexed say $subjects[-1]; # "Science" — last element say scalar(@subjects); # 3 — element count say $#subjects; # 2 — last index push @subjects, "Art"; # append my $p = pop @subjects; # remove & return last unshift @subjects, "PE"; # prepend my $s = shift @subjects; # remove & return first # Slices — extract multiple elements at once my @first_two = @subjects[0..1]; my @picked = @scores[0, 2, 4]; # Sorting — cmp is string sort, <=> is numeric my @alpha = sort @subjects; my @numer = sort { $a <=> $b } @scores; my @rev = reverse @alpha;
@subjects is the whole array. $subjects[0] is one element (sigil changes to $). @subjects[0,2] is a slice (sigil stays @).
qw(Mon Tue Wed) is exactly the same as ("Mon","Tue","Wed") — just faster to type.
push/pop work on the end (stack). unshift/shift work on the front (queue). Both return the removed element.
sort block, $a and $b are special — they're the two elements being compared. Never name your own variables $a or $b.
A hash stores key-value pairs — like a dictionary in Python or an object in JavaScript. It uses the % sigil. Keys are always strings; values are scalars. Order is not guaranteed, so use sort keys %hash for consistent output.
my %grade_letter = ( 'A' => 90, # => is the "fat comma" 'B' => 80, # it auto-quotes the left side 'C' => 70, # and reads as "maps to" 'D' => 60, 'F' => 0, ); my %info = ( name => "Alice Chen", gpa => 3.8, year => 2, ); # Access one value say $info{name}; # "Alice Chen" say $info{missing}; # undef (no error) # exists: is the key there? defined: is value not undef? if (exists $info{name}) { say "has name" } if (exists $info{phone}) { say "has phone" } # Add and delete $info{email} = 'a@school.edu'; delete $info{email}; # Iterate in sorted key order for my $key (sort keys %info) { printf " %-8s => %s\n", $key, $info{$key}; } # each() gives (key, value) pairs — for while loops while ((my ($k, $v) = each %grade_letter)) { say " $k : $v%"; }
%info. One value is $info{key} — the sigil shifts to $ because you're getting one scalar.
=> is just a comma that auto-quotes the left side. name => "Alice" and "name", "Alice" are identical — but => reads more like a map.
exists $h{k} — is the key in the hash at all?defined $h{k} — is the value not undef?A key can exist but have an
undef value.
sort keys %h for consistent, repeatable output.
Real programs need nested data. A hashref { } and arrayref [ ] create anonymous data structures stored as scalars. The -> arrow operator dereferences them. This is the most common pattern in Perl — arrays of hashrefs (like a list of records).
# An array of hashrefs — each element is a student record my @students = ( { name => "Alice Chen", scores => [92, 88, 95, 91, 87], email => 'alice@school.edu' }, { name => "Bob Martinez", scores => [74, 68, 72, 80, 65], email => 'bob@school.edu' }, # ... more students ... ); # array[i]{key} — element of array, then hash field say $students[0]{name}; # array[i]{key}[j] — nested array inside the hash say $students[0]{scores}[2]; # 95 (third score) # @{ } dereferences the arrayref into a real array my @alices_scores = @{ $students[0]{scores} }; say join(", ", @alices_scores); # 92, 88, 95, 91, 87 # Arrow notation is equivalent and often clearer say $students[0]->{name}; say $students[0]->{scores}->[2]; # Adjacent brackets: arrow between [] and {} is optional: say $students[0]{scores}[2]; # same thing
[1,2,3] creates an arrayref — a scalar holding an array. (1,2,3) creates a plain list. Use [ ] inside other data structures.
{key => val} creates a hashref. But { } also delimits code blocks. Perl infers which you mean from context. When in doubt, add a +: +{key=>val} always means hashref.
[0]{name} or {k}[0] — the -> is optional. Write whichever is clearest.
Perl subroutines receive all their arguments as a flat list in @_. There are no declared parameter types — you unpack @_ yourself at the top of the sub. The last expression evaluated is returned implicitly, but explicit return is clearer.
# Basic: unpack @_ into named variables sub calculate_average { my (@scores) = @_; return 0 unless @scores; # guard clause return sum(@scores) / scalar(@scores); } # Postfix if/unless as guard clauses is idiomatic sub letter_grade { my ($avg) = @_; return 'A' if $avg >= 90; return 'B' if $avg >= 80; return 'C' if $avg >= 70; return 'D' if $avg >= 60; return 'F'; } # Returning multiple values — Perl makes this natural sub score_stats { my (@scores) = @_; return ( avg => calculate_average(@scores), max => max(@scores), min => min(@scores), ); } # Receiving multiple return values into a hash my %stats = score_stats(80, 90, 85, 70); printf "avg=%.1f max=%d min=%d\n", $stats{avg}, $stats{max}, $stats{min}; # Named parameters: receive a hash of arguments sub make_report { my (%args) = @_; my $title = $args{title} // "Report"; # // = defined-or return "[$title]"; } make_report(title => "Grades", term => "Fall");
(1,2,3) and an array @a, they all merge. Always unpack first.
return 0 unless @scores; at the top of a sub is an idiomatic "early exit" — it prevents the rest of the function from running on bad input.
$x // $default returns $x if defined, otherwise $default. Better than || when 0 or "" are valid values.
Perl's conditionals should feel familiar, with two distinctive features: unless (which is "if not") and postfix modifiers that let you put the condition after the statement — great for guard clauses and short actions.
if ($avg >= 90) { say "Excellent"; } elsif ($avg >= 70) { say "Passing"; } else { say "Failing"; } # unless = "if not" unless ($avg >= 60) { say "Failing!"; }
# Condition goes AFTER the statement say "Pass" if $avg >= 60; say "Fail" unless $avg >= 60; # Great for guard clauses return if !defined $input; die "bad\n" if $error; # Ternary: COND ? TRUE : FALSE my $msg = $avg >= 60 ? "Passing" : "Failing";
elsif — not else if and not elif. Curly braces are always required; there is no braceless one-liner form like C has.
for and foreach are identical keywords. $_ is the implicit loop variable — when you don't give a name, that's what you get. last = break, next = continue, redo = restart iteration.
# foreach with a named variable — preferred for readability for my $student (@students) { printf "%-18s avg:%5.1f %s\n", $student{name}, $student{avg}, ($student{pass} ? "passing" : "FAILING"); } # C-style for — useful when you need the index for (my $i = 0; $i < @students; $i++) { say "[$$i] $students[$i]{name}"; } # while — condition checked before each iteration while (my $line = <STDIN>) { chomp $line; last if $line eq 'quit'; # break out of loop next unless $line; # skip empty lines say " Got: $line"; } # Postfix for — neat one-liners say " $_" for @names; # $_ = each element # Labels for nested loop control STUDENT: for my $s (@students) { for my $score (@{$s{scores}}) { if ($score < 65) { say "at risk: $s{name}"; next STUDENT; # jump to next $s } } }
These three functions transform lists functionally — they never modify the original. map transforms each element, grep filters, and sort orders. They compose naturally and replace most explicit loops. $_ is the current element inside the block.
# map: transform — runs block once per element, returns new list my @all_avgs = map { my @s = @{ $_{scores} }; # $_ = current student hash calculate_average(@s) } @students; # grep: filter — keeps elements where block returns true my @passing = grep { $_{pass} } @students; my @honor = grep { $_{avg} >= 90 } @students; my @failing = grep { !$_{pass} } @students; # sort with a comparison block ($a and $b are special) my @ranked = sort { $b{avg} <=> $a{avg} } @students; # Schwartzian Transform: efficient sort by computed key # 1. map: attach the key 2. sort 3. map: strip it off my @by_best = map { $_[0] } sort { $b[1] <=> $a[1] } map { [$_, max(@{$_{scores}})] } @students; # Build a frequency hash with map + a loop my %grade_count; $grade_count{ $_{grade} }++ for @students; # Grade distribution bar chart for my $g (sort keys %grade_count) { my $bar = "█" x $grade_count{$g}; printf " %s: %s (%d)\n", $g, $bar, $grade_count{$g}; }
Passing: 5 students Failing: 1 Honor roll: 2 Rankings (sorted by average): 1. Eve Patel 97.8 A 2. Alice Chen 90.6 A 3. Carol Williams 89.8 B 4. Bob Martinez 71.8 C 5. David Kim 61.0 D 6. Frank Johnson 45.8 F Grade distribution: A: ██ (2) B: █ (1) C: █ (1) D: █ (1) F: █ (1)
Regex is baked into the Perl language — it's not a library. =~ binds a string to an operation. // matches, s/// substitutes, tr/// transliterates. The /g flag in list context returns all matches at once.
# =~ tests a match. !~ tests non-match. if ($email =~ /\@/) { say "has @" } if ($email !~ /\d/) { say "no digits" } # /g in list context returns ALL matches as a list my @caps = ($text =~ /\b[A-Z]{2,}\b/g); say join(", ", @caps); # ALICE, MATH # Capture groups ( ) → $1, $2, ... if ($name =~ /^(\w+)\s+(.+)$/) { my ($first, $last) = ($1, $2); } # Named captures (?<name>) → %+ if ($date =~ /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/) { say "Year: $+{y} Month: $+{m}"; } # Substitution: s/PATTERN/REPLACEMENT/flags $str =~ s/foo/bar/; # replace first $str =~ s/foo/bar/g; # replace all $str =~ s/foo/bar/gi; # global + case-insensitive # /r flag: return copy instead of modifying $str my $new = $str =~ s/old/new/gr; # /e flag: evaluate replacement as Perl code $str =~ s/(\d+)/$1*2/ge; # double every number # Fix ALLCAPS to Title Case (my $clean = $text) =~ s/\b([A-Z]+)\b/\u\L$1/g; # tr///: character mapping (not regex — char-by-char) $str =~ tr/a-z/A-Z/; # uppercase all my $vowels = ($str =~ tr/aeiou//); # count vowels $str =~ tr/aeiou//d; # /d = delete matches
All-caps words: ALICE, MATH Date parsed: year=2025 month=09 day=15 Original: student scored EXCELLENT results in all SUBJECTS today. Fixed: student scored Excellent results in all Subjects today. Template: Alice avg is 90.6 and Bob avg is 71.8. ROT13 of 'Hello World': Uryyb Jbeyq
A reference is a scalar that holds the memory address of another value. Use \ to reference an existing variable, or [ ] / { } to create anonymous data inline. Code references — sub { } — let you store functions in variables.
# Creating references my $aref = \@array; # reference to existing array my $href = \%hash; # reference to existing hash my $anon = [1, 2, 3]; # anonymous arrayref my $rec = {name => "Alice"}; # anonymous hashref # Arrow notation to dereference say $aref->[0]; # array element via ref say $rec->{name}; # hash value via ref # @{} and %{} dereference the whole structure my @copy = @{$aref}; # ref() returns the type of reference say ref($aref); # "ARRAY" say ref($rec); # "HASH" # Code references — storing subs in variables (lambdas) my $square = sub { $_[0] ** 2 }; say $square->(5); # 25 # Dispatch table: hash of code refs my %actions = ( 'A' => sub { "Excellent!" }, 'B' => sub { "Good job." }, 'F' => sub { "Please see me." }, ); my $msg = $actions{ $grade }->(); # call by grade letter # Passing by reference — modifies the original sub double_all { my ($ref) = @_; $_ *= 2 for @{$ref}; } double_all(\@scores); # @scores is modified
A closure is a subroutine that captures variables from its surrounding scope — it "closes over" them, keeping them alive even after the outer function returns. This is Perl's lambda / function factory. state variables persist between calls without being globals.
# Factory function — each call returns a NEW closure # with its own private copy of $min_avg sub make_grade_filter { my ($min_avg) = @_; return sub { # $min_avg is "closed over" my ($student) = @_; return $student{avg} >= $min_avg; }; } # Each closure remembers its own $min_avg independently my $is_honor = make_grade_filter(90); my $is_passing = make_grade_filter(60); my $is_at_risk = make_grade_filter(70); my @honor_roll = grep { $is_honor->($_) } @students; my @at_risk = grep { !$is_at_risk->($_) } @students; # state: variable persists between calls (like a private static in C) use feature 'state'; sub running_average { my ($new_val) = @_; state $count = 0; # initialized once, remembered forever state $total = 0; $count++; $total += $new_val; return $total / $count; } # Each call updates the running total printf "Running avg: %.1f\n", running_average($_{avg}) for @students;
sub captures $min_avg. Even after make_grade_filter returns, $min_avg lives on inside the closure. Each call creates a separate $min_avg.
my $x resets to the initial value on every call. state $x keeps its value across calls — like a private static variable.
Honor roll: Alice, Eve At risk: David, Frank Running stats (state variables): After Alice : n=1 running_avg=90.6 After Bob : n=2 running_avg=81.2 After Carol : n=3 running_avg=84.1 After David : n=4 running_avg=78.3 After Eve : n=5 running_avg=82.2 After Frank : n=6 running_avg=76.1
Always use the three-argument form of open(). The or die $! idiom handles failures by printing the system error message stored in $!. Always close() explicitly when done.
# Writing — '>' truncates, '>>' appends, '<' reads open(my $fh, '>', 'report.txt') or die $!; open(my $fh, '>>', 'log.txt') or die $!; open(my $fh, '<', 'data.txt') or die $!; # Writing: print to a filehandle (no comma!) print $fh "line of text\n"; printf $fh "%-15s %s\n", "Label:", $value; # Reading line by line — most memory-efficient while (my $line = <$fh>) { chomp $line; # removes trailing newline # process $line... } # Slurp all lines into an array my @lines = <$fh>; chomp @lines; # chomp works on arrays too # Slurp entire file into one string my $content = do { local $/; <$fh> }; # local $/ = undef disables line-splitting for this block close $fh; # always close when done # Diamond <> reads from @ARGV files or STDIN while (<>) { print } # cat-like: prints every line # File test operators — check without opening -e $path # exists -f $path # is a plain file (not dir, symlink...) -d $path # is a directory -r $path # readable by current user -s $path # file size in bytes -M $path # age in days since last modification
eval { } is Perl's try block. die throws an exception. The caught value lands in $@. You can die with a string or with a reference (for structured exceptions with typed fields). warn prints to STDERR but doesn't stop execution.
sub safe_divide { my ($a, $b) = @_; # \n suppresses "at line N" suffix die "Division by zero!\n" if $b == 0; return $a / $b; } my $result = eval { safe_divide(10, 0); }; if ($@) { print "caught: $@"; }
# Die with a hashref for typed exceptions die { type => "FileNotFound", file => $filename, message => "No such file", }; eval { risky() }; if (my $e = $@) { if (ref($e) eq 'HASH') { say $e->{type}; say $e->{message}; } else { die $e; # re-throw unknown } }
OK: 10 / 2 = 5.00 CAUGHT: 7/0 — Division by zero! OK: 15 / 4 = 3.75 [FileNotFound] ghost_file.csv: The file does not exist
Perl's OOP is built on three primitives: a package is a class, bless() ties a hashref to a class name making it an object, and any subroutine in that package is a method. The first argument to every method is the object itself — called $self by convention.
package Student; use parent # for inheritance later # Constructor — 'new' is convention, any name works sub new { my ($class, %args) = @_; # $class = "Student" die "name required\n" unless defined $args{name}; my $self = { name => $args{name}, email => $args{email}, scores => $args{scores} // [], }; return bless $self, $class; # bless makes it an object } # Accessor: get/set name sub name { my ($self, $new) = @_; $self->{name} = $new if defined $new; return $self->{name}; } # Method chaining: return $self to chain calls sub add_score { my ($self, $score) = @_; push @{ $self->{scores} }, $score; return $self; # enables chaining } # --- Subclass with inheritance --- package HonorsStudent; use parent -norequire, 'Student'; sub new { my ($class, %args) = @_; my $self = $class->SUPER::new(%args); # call Student::new $self->{bonus} = $args{bonus} // 5; return $self; } package main; # Method chaining my $s = Student->new(name => "Grace", email => 'g@s.edu') ->add_score(88)->add_score(92)->add_score(95); ref($s); # "Student" $s->isa('Student'); # 1 = true $s->can('add_score'); # coderef or undef
Grace Lee avg:91.7 grade:A Henry Park avg:87.3 grade:B [Honors +5pts] Grace Lee class=Student isa Student=yes Henry Park class=HonorsStudent isa Student=yes
Perl uses punctuation variables for things most languages need keywords for. The most important is $_ — the default variable used implicitly by map, grep, for, print, chomp, and many other operations.
| Variable | Meaning | When to use it |
|---|---|---|
| $_ | Default variable | Loop body, map/grep block, print, chomp — implicit everywhere |
| @_ | Sub arguments | Inside every subroutine — always unpack first |
| $! | System error | After a failed open, rename, or other OS call |
| $@ | eval error | After an eval { } block to check if it threw |
| $? | Child process exit | After system() or backtick command |
| $/ | Input record separator | Set to undef to slurp files; default is \n |
| $0 | Script name | Useful in error messages and log lines |
| @ARGV | Command-line args | Arguments passed to the script on the command line |
| %ENV | Environment variables | $ENV{PATH}, $ENV{HOME}, etc. |
| $. | Current line number | While reading a file with <$fh> |
# $_ in action — used implicitly my @words = qw(apple BANANA cherry); for (@words) { $_ = lc; # lc with no args operates on $_ print; # print with no args prints $_ } # wantarray: detect caller's context sub flexible { return wantarray ? (1,2,3) : "all-in-one"; } my @list = flexible(); # list context → (1, 2, 3) my $scalar = flexible(); # scalar context → "all-in-one" # Context affects built-ins too my @arr = (5,3,8,1); my $count = @arr; # 4 — scalar context = count my ($first) = @arr; # 5 — list context (parens!) = first element # Command-line args and environment my $arg1 = $ARGV[0] // "default"; my $home = $ENV{HOME}; my $path = $ENV{PATH};
Class: Introduction to Programming Teacher: $teacher Teacher: Ms. Hernandez ================================================== Class: Introduction to Programming — Fall 2025 Semester: Fall 2025 Weeks: 16 Days: 80 Hours: 120.0 STUDENT REPORT (6 students) ------------------------------------------------------------ Alice Chen avg: 90.6 ⭐ passing Bob Martinez avg: 71.8 ~ passing Carol Williams avg: 89.8 ✓ passing David Kim avg: 61.0 △ passing Eve Patel avg: 97.8 ⭐ passing Frank Johnson avg: 45.8 ✗ FAILING Rankings (sorted by average): 1. Eve Patel 97.8 A 2. Alice Chen 90.6 A 3. Carol Williams 89.8 B 4. Bob Martinez 71.8 C 5. David Kim 61.0 D 6. Frank Johnson 45.8 F Grade distribution: A: ██ (2) B: █ (1) C: █ (1) D: █ (1) F: █ (1) REGULAR EXPRESSIONS All-caps words: ALICE, MATH Date parsed: year=2025 month=09 day=15 Fixed: student scored Excellent results in all Subjects today. REFERENCES Grade messages: Alice (A): Excellent — scholarship eligible! Frank (F): Failing — mandatory intervention. CLOSURES Honor roll: Alice, Eve Running avg after all 6: 76.1 FILE I/O Report written → grade_report.txt CSV written → students.csv Lines read: 18 Words: 83 ERROR HANDLING OK: 10 / 2 = 5.00 CAUGHT: 7/0 — Division by zero! [FileNotFound] ghost_file.csv: The file does not exist OBJECT-ORIENTED PERL Grace Lee avg:91.7 grade:A Henry Park avg:87.3 grade:B [Honors +5pts] ============================================================ TOUR COMPLETE — 17 language features covered ============================================================