perl.gg / functional

Dispatch Tables

2024-06-30

Part 1: THE PROBLEM WITH IF-ELSE CHAINS

Look, we've all been there. You start with a simple conditional:
if ($color eq 'blue') { say 'The sky weeps'; } elsif ($color eq 'red') { say 'Blood on the horizon'; } elsif ($color eq 'green') { say 'Envy grows'; }
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.

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:

my $dispatch = { blue => sub { say 'The sky weeps' }, red => sub { say 'Blood on the horizon' }, green => sub { say 'Envy grows' }, };
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.

To invoke:

$dispatch->{$color}->();
Or, if you're feeling fancy (and you should be):
$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.

my $greet = sub { say "Hello, $_[0]" }; $greet->('World');
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.

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:

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
Now THAT'S a higher-order function returning a closure. More on closures in another tutorial.

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:

# 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);
The declarative version separates the definition of operations from their invocation. You can add operations without touching the invocation code.

Part 4: PRACTICAL PATTERNS

DISPATCH WITH DEFAULT
my $dispatch = { foo => sub { ... }, bar => sub { ... }, }; my $handler = $dispatch->{$key} // sub { warn "Unknown: $key" }; $handler->();
DISPATCH WITH ARGUMENTS
my $math = { add => sub { my ($a, $b) = @_; return $a + $b }, mul => sub { my ($a, $b) = @_; return $a * $b }, }; my $result = $math->{$op}->($x, $y);
DISPATCH WITH METHOD NAMES (stringly typed, but sometimes useful)
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:

Use them when:

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.

"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman Dispatch tables are readable. Use them.
perl.gg