Dispatch Tables
Part 1: THE PROBLEM WITH IF-ELSE CHAINS
Look, we've all been there. You start with a simple conditional:And then requirements come in. More colors. More behaviors. Before you know it, you've got a 200-line if-elsif-elsif-elsif monster that makes your eyes bleed and your soul weep.if ($color eq 'blue') { say 'The sky weeps'; } elsif ($color eq 'red') { say 'Blood on the horizon'; } elsif ($color eq 'green') { say 'Envy grows'; }
There's a better way. And it's been sitting in Perl since... well, forever.
Part 2: ENTER THE DISPATCH TABLE
A dispatch table is exactly what it sounds like: a lookup table that dispatches control to the appropriate handler. In Perl, we build these using hash references where the values are subroutine references.Behold:
That's it. That's the whole thing. Each key maps to an anonymous subroutine. The curly braces create a hash reference. The sub { } creates a code reference.my $dispatch = { blue => sub { say 'The sky weeps' }, red => sub { say 'Blood on the horizon' }, green => sub { say 'Envy grows' }, };
To invoke:
Or, if you're feeling fancy (and you should be):$dispatch->{$color}->();
$dispatch->{$color}->() if exists $dispatch->{$color};
Part 3: WHY THIS IS ACTUALLY FUNCTIONAL PROGRAMMING
"But wait," you say, "I thought functional programming was all Haskell and monads and category theory." Nope. The core ideas are simpler than that.FIRST-CLASS FUNCTIONS In Perl, subroutines are values. You can store them in variables, pass them to other subroutines, return them from subroutines. They're data.
That $greet isn't special. It's just a scalar holding a reference. You could put it in an array. You could put it in a hash. You could pass it to map.my $greet = sub { say "Hello, $_[0]" }; $greet->('World');
HIGHER-ORDER FUNCTIONS A higher-order function is one that takes functions as arguments or returns them. Our dispatch table is a data structure of functions, and the lookup mechanism is essentially a higher-order operation.
Consider this factory:
Now THAT'S a higher-order function returning a closure. More on closures in another tutorial.sub make_dispatch { my %handlers = @_; return sub { my $key = shift; return $handlers{$key}->(@_) if exists $handlers{$key}; die "Unknown key: $key"; }; } my $dt = make_dispatch( add => sub { $_[0] + $_[1] }, mul => sub { $_[0] * $_[1] }, ); say $dt->('add', 2, 3); # 5 say $dt->('mul', 4, 5); # 20
DECLARATIVE STYLE Notice how the dispatch table declares WHAT should happen, not HOW to decide what should happen? That's the declarative style. The control flow is implicit in the data structure itself.
Compare:
The declarative version separates the definition of operations from their invocation. You can add operations without touching the invocation code.# Imperative (HOW) if ($op eq 'add') { $result = $a + $b } elsif ($op eq 'sub') { $result = $a - $b } elsif ($op eq 'mul') { $result = $a * $b } # Declarative (WHAT) my $ops = { add => sub { $_[0] + $_[1] }, sub => sub { $_[0] - $_[1] }, mul => sub { $_[0] * $_[1] }, }; $result = $ops->{$op}->($a, $b);
Part 4: PRACTICAL PATTERNS
DISPATCH WITH DEFAULTDISPATCH WITH ARGUMENTSmy $dispatch = { foo => sub { ... }, bar => sub { ... }, }; my $handler = $dispatch->{$key} // sub { warn "Unknown: $key" }; $handler->();
DISPATCH WITH METHOD NAMES (stringly typed, but sometimes useful)my $math = { add => sub { my ($a, $b) = @_; return $a + $b }, mul => sub { my ($a, $b) = @_; return $a * $b }, }; my $result = $math->{$op}->($x, $y);
my $actions = { create => 'handle_create', update => 'handle_update', delete => 'handle_delete', }; my $method = $actions->{$action}; $self->$method(@args);
Part 5: WHEN NOT TO USE DISPATCH TABLES
Dispatch tables aren't always the answer. Don't use them when:- You have 2-3 simple cases (just use if-else, it's fine)
- The conditions aren't simple equality checks
- You need complex pattern matching (though you can combine with given/when)
- The "table" would only be used once (overhead isn't worth it)
Use them when:
- You have many cases with clean key-value mapping
- Cases might be added/removed dynamically
- You want to separate definition from execution
- You're building plugin systems or extensible architectures
Part 6: THE PHILOSOPHICAL BIT
The dispatch table pattern reveals something profound about Perl: it treats code as data and data as code with equal facility. There's no special "function type" that's walled off from the rest of the language. A subref is just another scalar value.This is why Perl people get annoyed when someone says Perl "isn't a real functional language." We've been passing functions around, building higher-order abstractions, and writing declarative code since 1987.
We just don't make a big deal about it.
perl.gg"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman Dispatch tables are readable. Use them.