The Hidden Power of Prototypes
Introduction
I have been leaning very heavily on Perl's prototype feature for creating new modules. The imptetus for this can traced back to the day I looked at the source code for Try::Tiny
, and realized that it was implemented using prototypes. The main reason for using prototypes for many new modules I've created recently is my focus on making a thing I do repeatedly available in a more Perlish or idiomatic way.
Required reading if you have not and are about to dive into an article about prototypes: Far More Than Everything You've Ever Wanted to Know about Prototypes in Perl by Tom Christiansen.
The following article demonstrates 2 CPAN modules I have written that focus more on Perl programmer UX and why Perl prototypes can provide a way forward for a great many ideas that people have. Prototypes misunderstood, yes; but more so, they are misunderestimated. They are infact, very powerful and when used for their intended purpose; a lot of hand wringing can be avoided during feature discussions.
Recent Examples on CPAN
Two such examples that I've written and uploaded to CPAN (PAUSE ID: OODLER) are:
Try::ALRM
(try/catch/retry like semantics foralarm
andALRM
handlingDispatch::Fu
- oneHASH
based dispatcher to rule them alltm
Try::ALRM
I discussed Try::ALRM
in a 2022 Perl Advent article, so I won't go into it other than to say that it's an idiomatic way to deal with alarm
in the same way as die
is handled by Try::Tiny
. Once I realizeed that an ALRM
signal was thrown like an exception and handled directly by $SIG{ALRM}
, it made perfect sense to have a try kind of interface. The benefit to this kind of UX alignment is that it makes a retry
extension to the structure very natural and easy.
try_once { my $nread = sysread $socket, $buffer, $size; } finally { my ($attempt, $successful) = @_; if (not $successful) { ... timed out } else { ... didn't } } timeout => $timeout;
Dispatch::Fu
I recently uploaded a module to CPAN called Dispatch::Fu
because I was watching a lot of hand wringing (more than usual) on the p5p
mailing list abou smartmatch, given
/when
, and different kinds of switch statements.
Having grappled with this problem from a different angle before, I felt like I recognized all of these cases as more general forms of something a lot of Perl developers learn to love, i.e., HASH
based dispatching. This can be especially powerful for converting a long if
/elsif
/else
chain (that is technically O(num_branches)
) into a constant time O(1)
HASH
look up if there is a static string suitable for use as a HASH
key. For example, this if
block can be converted to a HASH
dispatch table quite readily:
if ($cgi->param("action") eq q{show_form}) { ... } elsif ($cgi->param("action") eq q{process_form}) { ... } elsif ($cgi->param("action") eq q{show_thankyou_html}) { ... } else { ... }
Would become:
my $action = $cgi->param("action"); my $actions = { show_form => sub { ... }, process_form => sub { ... }, show_thankyou_html => sub { ... }, default => sub { ... }, # the 'else' BLOCK } $action = q{default} if (not $action or not exists $actions->{$action}); $actions->{$action}->();
The Problem with Traditional Dispatching
This works because $action
is in this case (and of is in old school CGI.pm
applications), a proper string. But idiom quickly breaks down in the following case,
if ($cgi->param("action") eq q{foo} and $cgi->param("userid") != 0) { ... } elsif ($cgi->param("action") eq q{show_thankyou_html}) { ... } else { ... }
The Solution to Complicated Dispatching
Dispatch::Fuwas created to expose, in a _Perlish_ and structure way, a pattern that allows the programmer to reduce an arbitrary condition into a specific named case, then dispatch the action based on that. Using `Dispatch::Fu`, this would turn into the following:
use Dispatch::Fu; dispatch { my $cgi = shift; if ($cgi->param("action") eq q{foo} and $cgi->param("userid") != 0) { return "foo"; } elsif ($cgi->param("action") eq q{show_thankyou_html}) { return "thanks"; } else { return "default"; } } $cgi, on foo => sub { ... }, on thanks => sub { ... }, on default => sub { ... };
Given that example, readers should be able to see the value in this approach. The implementation of Dispatch::Fu
is tiny and it is fast. All of the complexity is also put onto the shoulders of the programmer in how they implement the dispatch
BLOCK that is used to determine what case name to return for immediate execution.
How Does Dispatch::Fu
Work?
The remainder of this article will describe how Dispatch::Fu
uses Perl prototypes to work. At the time of this writing, Dispatch::Fu
in its entirety follows,
package Dispatch::Fu; use strict; use warnings; use Exporter qw/import/; our $VERSION = q{0.95}; our @EXPORT = qw(dispatch on); our @EXPORT_OK = qw(dispatch on); sub dispatch (&@) { my $code_ref = shift; # catch sub ref that was coerced from the 'dispatch' BLOCK my $match_ref = shift; # catch the input reference passed after the 'dispatch' BLOCK # build up dispatch table for each k/v pair preceded by 'on' my $dispatch = {}; while ( my $key = shift @_ ) { my $HV = shift @_; $dispatch->{$key} = _to_sub($HV); } # call $code_ref that needs to return a valid bucket name my $key = $code_ref->($match_ref); die qq{Computed static bucket not found\n} if not $dispatch->{$key} or 'CODE' ne ref $dispatch->{$key}; # call subroutine ref defined as the v in the k/v $dispatch->{$key} slot return $dispatch->{$key}->(); } sub on (@) { return @_; } sub _to_sub (&) { shift; } 1;
Before I begin to explain what is happening, it is important to clearly define what a Perl prototype is not and what it is.
- Perl prototypes is not a system for creating named parameters
- Perl prototypes is not a system for describing parameter signatures
- Perl prototypes is a way to describe Perl data type coersions
- Perl prototypes is a way to achieve Perl keyword-like or built-in functions calling UX
- Perl prototypes does use declarative, templated DSL+
(+Much like (distinctly) with pack
or printf
)
This table from perlsub
is extremely informative and pretty much says it all, which is a lot!:
> perldoc perlsub ... **Declared as Called as** sub mylink ($$) mylink $old, $new sub myvec ($$$) myvec $var, $offset, 1 sub myindex ($$;$) myindex &getstring, "substr" sub mysyswrite ($$$;$) mysyswrite $buf, 0, length($buf) - $off, $off sub myreverse (@) myreverse $x, $y, $z sub myjoin ($@) myjoin ":", $x, $y, $z sub mypop (\@) mypop @array sub mysplice (\@$$@) mysplice @array, 0, 2, @pushme sub mykeys (\[%@]) mykeys $hashref->%* sub myopen (*;$) myopen HANDLE, $name sub mypipe (**) mypipe READHANDLE, WRITEHANDLE sub mygrep (&@) mygrep { /foo/ } $x, $y, $z sub myrand (;$) myrand 42 sub mytime () mytime
What this table doesn't tell you, is that you may magnify the effect of prototypes by combining them together in creative ways. It also fails to suggest that you may create utility methods that do thing like convert a bare lexical block into a subroutine reference. For example, using the method defined with a prototype above, _to_sub
, this works as expected:
my $CODE_ref = _to_sub { my @ARGS = @_ return printf qq{Hi, there: %s!!!\n}, join('and ', @ARGS); }; ... $CODE_ref->(qw/Billy Jean/);
And how doe that work? Consider the suboutine, _to_sub
:
sub _to_sub (&) { shift; }
The ampersand &
in _to_sub(&)
is a template that tells perl
to convert any thing passed in a lexical block into a subroutine reference. MAGIC HAPPENS. Then the updated @_
is available to shift
. For the uninitiated, the above is explicitly equivalent to:
sub _to_sub (&) { my $code_ref = shift; return $code_ref; }
So what's shift
'd to $code_ref
(i.e., the lexical block, { ... }
) has already been coerced into a CODE
reference. Yes, it's MAGIC. Another example of composing prototypes is the implementation of the on
keyword:
sub on (@) { return @_; }
In the context of other keywords, this is a null accumulator there merely for the UX of the whole thing. I.e., it just keeps rolling things to its right into an array. E.g.,
dispatch { ... } $cgi, on foo => sub { ... }, on thanks => sub { ... }, on default => sub { ... };
Could validly be written as,
dispatch { ... } $cgi, foo => sub { ... }, thanks => sub { ... }, default => sub { ... };
If you are thinking on
is just a hint for mere mortals, then you are correct!
And now it can be pointed out that dispatch
is really just set up to take a list of things; the first thing is a CODE
reference (coerced by &
), the second thing is the SCALAR
reference that is passed to $code_ref
as its only parameter (see dispatch
's implementation above) that is rolled into @
; then the rest of @
, which contains all the case/sub
pairs:
sub dispatch (&@) { my $code_ref = shift; # captures lexical block, { ... } my $match_ref = shift; # captures $CASES ...
There rest of the call just builds up the $dispatch
HASH
reference, calls $code_ref
by passing in $match_ref
, then assumes $code_ref
will return a key that is contained in $dispatch
. Then that key is used to call the subroutine assumed to be stored in $dispatch
by reference. Thats it!
More Hidden Features of Prototypes Remain
In all my years, I have not seen a lot written on prototypes other than FUD. I certainly have not seen anything that aimed to expose interesting structures or even classes of structures. I believe I may have just scratched the surface of what is possible, and I encourage others to explore the hidden taxonomy of structures that seems to be lurking just below the surface.
For example, the general structure of Dispatch::Fu
is covered in the table above as mygrep { /foo/ } $x, $y, $z
. The on
keyword is effectively putting syntactical sugar around the creation of the list on the lefthand side of the mygrep
BLOCK.
The other thing to not with this structure is that there is no comma immediately after the BLOCK, but commas are required there after. Using on
, the requirement to have a successive trail of commas (recall =>
is equivalent to a comma, often called the Texas comma) is broken; the pattern is therefore:
dispatch BLOCK REF, on string1 => CODE, on string2 => CODE, ..., on stringN => CODE;
But the accumulator, on(@)
as defined effectively works to filter out the on
. I think that is an interesting effect, and the current system can be exploited to do some very interesting things with syntax structure. Which means it is equivalent to,
dispatch BLOCK REF, string1, CODE, strint2, CODE, ..., stringN, CODE;
And this is where the mygrep
pattern becomes apparent:
mygrep {...} $x, $y, $z, ...;
If you have noticed or done anything interesting to exploit Perl prototype syntax in interesting or surprising ways, please let everyone know in the comments below. Untill next time, take care!
It's always the
&
prototype that's useful IME, the other never really are.That is definitely the cornerstone one, I am looking at combinations of prototypes to find interesting structures. That said, you may be right that there is nothing really interesting to find.
another interesting/useful prototype is `*`,
sub foo (*) {
my $fh = qualify_to_ref(shift, caller);
...
}
which lets you do stuff like this
my_name(Roger);
my_name(Peter);
That seems interesting in the sense of “may you live in interesting times” 😛
Another possibly useful prototype is the underscore (“
_
”), which allows functions to default to$_
when given no argument, like e.g.chr
andhex
do.I agree that prototypes are a very useful tool. Try::ALRM is genuinely useful and interesting.
I see the main limitation on prototypes being that & only coerces on the first parameter. I don't see a downside to & coercion on subsequent parameters. If the prototype says it should be a code ref, then if it looks like a block, treat it like a block, same as if the "sub" were there.
I don't see much advantage to Dispatch::Fu. While it adds a little syntactic sugar, it's really just an indirect way to do:
if ($cgi->param("action") eq q{foo} and $cgi->param("userid") != 0) {
do_foo();
}
elsif ($cgi->param("action") eq q{show_thankyou_html}) {
do_thanks();
}
else {
do_default();
}
Or just dispatch on $cgi->param("action") then add qualifying logic as part of the code blocks.
> I don't see much advantage to Dispatch::Fu. While it adds a little syntactic sugar, it's really just an indirect way to do...
That's the point, but it's more than sugar! The computational complexity of determining the dispatch "key" is entirely contained in the "dispatch" block and is under the control of the developer. Once the "key" is determined, the proper method is called as O(1)!
Thanks for all to good info above, didn't know bout the "_" allowing one to default to "$_" - I was actually wondering about that recently!
I am not good at coming up with contrived examples as evidenced in this Perl Advent article,
https://perladvent.org/2023/2023-12-13.html
But a set of cascading "if" blocks is not the most exciting example. However, any code could go in there to compute the key to then "call".