הלינקייה: מגזין חודשי למפתחים

רוצה לשמוע על כל האירועים, המדריכים, הקורסים והמאמרים שנכתבו החודש ?
הלינקייה הינו מגזין חופשי בעברית שמשאיר אותך בעניינים.
בלי ספאם. בלי שטויות. פעם בחודש אצלך בתיבה.

Subroutine Prototypes

The Problem

Some perl native functions may seem magical. Take push for instance, that function takes a list and a list of elements, and inserts the list of elements to the end of its first parameter.

If we try to write a push, we will very soon realize the problem - inside the subroutine, @_ contains all the arguments it got, and we cannot discriminate the original list from the elements to add to it. When we take a moment to think about push, we come to the inevitable conclusion that the only way it can work is by taking a reference to a list as its first parameter. Surprisingly enough, this is exactly what happens.

When we call push, the compiler magically adds a \ before the first argument, resulting in a LISTREF passed as the first element of @_. Instructing the compiler to act this way is easy - when declaring a subroutine, we add parenthesis after the subroutine name and before the curly bracket that starts its body. Inside the parenthesis, we use a special syntax to tell the compiler how to modify our arguments. For a complete list of options there, check man perlsub.

Let's see some examples of subroutine prototyping.

use strict;
use warnings;
 
=pod
combine method takes two lists and returns a new list, so that every
element in the new list is the subtraction of the corresponding elements
from input lists. 
 
We use prototype here so caller won't need to know we take inputs by-ref
=cut
 
sub combine(\@\@) {
	my ($r_l1, $r_l2) = @_;
	my $i = 0;
 
	# turn off warnings for cases lists are not same size
	no warnings;
	return map { $_ - $$r_l2[$i++] } @$r_l1;
}
 
##############################################################
 
=pod
This one works even better. It takes a CODEREF and combines
both lists using that CODEREF
=cut
sub combine2(&\@\@) {
	my ($f, $l1, $l2) = @_;
	my $i = 0;
	die "Must provide a valid CODEREF as first parameter" 
		unless (ref $f) =~ /CODE/;
 
	die "Must provide two lists" 
		unless (ref $l1) =~ /ARRAY/ && (ref $l2) =~ /ARRAY/;
 
	# turn off warnings for cases lists are not same size
	no warnings;
 
	return map { $f->($_, $$l2[$i++]) } @$l1;
}
 

When (Not) To Use

Prototypes may seem like a good idea at first site, but do note they have some critical limitations. Here they are:

  • Prototypes happen at compile time. This means any function that is called dynamically will not get prototyped.
  • Prototypes can be wrong. If you put a prototype asking for a list ref, and I send a list ref the compiler will complain. It actually expected a normal list so it could add the \ itself.
  • Prototypes are confusing Most of the perl code we read and write will not have prototypes. When we encounter a subroutine that is called on a list, we do not expect it to modify the list (unless, of course, we're talking about the core perl functions such as push). This means the caller needs to be aware there's a prototype involved, and this can lead to bugs

For all the reasons above, I highly advise to Refrain from using prototypes at almost all places

Ok, so when is it ok to use ?

There's one place that prototypes make sense - and that is to define constants. Let's examine the following:

sub PI { 3.14 }

print PI + 2;

Surprisingly, this prints 3.14. The reason is perl wants to give +2 as an argument to the PI function, not understanding that PI expects no arguments.
When we want to define a constant subroutine, using a prototype actually makes sense. Here's the modified example:

sub PI() { 3.14 }

# Now prints 5.14
print PI + 2;
course: