◈ Netsurfer / perl.gg / Dispatch Tables

Dispatch Tables

2024-06-30

Part 1: THE PROBLEM WITH IF-ELSE CHAINS

This page shows how to replace long chains of if-else blocks with a dispatch table in Perl. You start with a simple conditional check like the example below.
if ($color eq 'blue') { say 'The sky weeps'; } elsif ($color eq 'red') { say 'Blood on the horizon'; } elsif ($color eq 'green') { say 'Envy grows'; }
As requirements grow, you add more colors and behaviors. The chain eventually becomes a long, hard-to-maintain block of elsif statements.

A dispatch table handles these cases with less code. This pattern has been available in Perl for a long time.

Part 2: ENTER THE DISPATCH TABLE

A dispatch table is a lookup structure that sends control to the correct handler. In Perl, you build one using a hash reference where the values are subroutine references.
my $dispatch = { blue => sub { say 'The sky weeps' }, red => sub { say 'Blood on the horizon' }, green => sub { say 'Envy grows' }, };
Each key maps to an anonymous subroutine. The curly braces create a hash reference. The sub { } creates a code reference.

To invoke the table:

$dispatch->{$color}->();
Or, if you want to check for existence first:
$dispatch->{$color}->() if exists $dispatch->{$color};

Part 3: WHY THIS IS ACTUALLY FUNCTIONAL PROGRAMMING

Functional programming often sounds like a set of abstract rules, but 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, or return them from subroutines. They are data.

my $greet = sub { say "Hello, $_[0]" }; $greet->('World');
That $greet is not special. It is 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
That is a higher-order function returning a closure. We cover closures in another tutorial.

DECLARATIVE STYLE Notice how the dispatch table declares what should happen, not how to decide what should happen? That is 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 are not always the answer. Do not use them when:
  • You have 2-3 simple cases (just use if-else, it is fine)
  • The conditions are not simple equality checks
  • You need complex pattern matching (though you can combine with given/when)
  • The "table" would only be used once (overhead is not worth it)

Use them when:

  • You have many cases with clean key-value mapping
  • Cases might be added or removed dynamically
  • You want to separate definition from execution
  • You are building plugin systems or extensible architectures

Part 6: THE PHILOSOPHICAL BIT

The dispatch table pattern reveals something about Perl: it treats code as data and data as code with equal facility. There is no special "function type" that is 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 have been passing functions around, building higher-order abstractions, and writing declarative code since 1987.

We just do not 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
✓ Document: Donefunctional / 9.1 KiB▧