Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl: the Markov chain saw
 
PerlMonks  

Gratuitous use of Perl Prototypes

by grantm (Parson)
on Nov 08, 2004 at 22:39 UTC ( [id://406231]=perlmeditation: print w/replies, xml ) Need Help??

I recently had cause to resurrect my sermon against inappropriate use of Perl prototypes at a Perl Mongers meeting. I thought it might be of interest to some here too.

But first, let's start with the case *for* appropriate use of Perl prototypes. Imagine that you wanted to write a function that wrapped Perl's builtin function 'push' but forced everything being pushed onto the array to upper case. Your starting position might be this:

sub PUSH { my(@array, @new_items) = @_; push(@array, map { uc } @new_items); } my @target = ('RED', 'GREEN'); PUSH(@target, 'blue');

This obviously won't work since all arguments passed to PUSH() will end up in @array and @new_items will end up empty. Another reason it won't work is that the push function is operating on @array which only exists within the PUSH function and doesn't affect the original array (@target).

One way to make it work is to pass in an array reference instead of an array:

sub PUSH { my($array, @new_items) = @_; push(@$array, map { uc } @new_items); } my @target = ('RED', 'GREEN'); PUSH(\@target, 'blue');

This is fine, but it does require a little more work from the caller (actually one backslash) and it's not quite as tidy as Perl's builtin 'push'.

Prototypes allow your functions to behave like Perl's builtins. In our example, we can use a prototype to declare that our PUSH function needs one arrayref, followed by an arbitrarily long list of arguments:

sub PUSH (\@@) { my($array, @new_items) = @_; push(@$array, map { uc } @new_items); } my @target = ('RED', 'GREEN'); PUSH @target, 'blue';

Now when we call PUSH, we specify an array as the first argument and Perl coerces that into the arrayref that PUSH requires.

Even though this is what Perl prototypes are intended to be used for, it could be argued that even this use is a bad thing, since it might lead to unexpected consequences. Usually in Perl if you pass an array to a subroutine, you don't expect the array to be changed. If a subroutine expects an arrayref, then that might be taken as a signal that the routine possibly intends to modify the referenced array. Prototypes allow a programmer to hide the fact that the routine will get an arrayref.

Now let's open the case for the prosecution. Let's assume that we have a persistent whiney developer who is determined to use prototypes on every Perl subroutine and is attempting to defend his choice:

1. Prototypes allow Perl to pick up errors in subroutine calls.

This is a common mistake made by developers who have a background with 'C', since this is exactly what 'C' prototypes are for. It is unfortunate that Perl uses the name 'prototype', since in Perl, the feature exists for an entirely different reason - to allow Perl to silently coerce the arguments into a form that matches the prototype. In fact Perl's compiler will only throw errors if it can't manage to do that.

2. Prototypes are a useful way of documenting a subroutine's expectations.

Perhaps, but a subroutine that starts like this:

sub border_style ($$$) {

only tells us that it expects three arguments, whereas a subroutine that starts like this:

sub border_style { my($width, $style, $colour) = @_;

tells us not only that the subroutine expects three arguments, but also how it is planning to interpret each argument.

Since you're probably going to assign your arguments to variables with meaningful names anyway, the prototype is essentially redundant. If you need more documentation, then POD is probably the right tool for the job.

3. Well at least I know my methods are being called with the right number of arguments.

Whoa there! Did you say 'methods'? Perl does not attempt to check prototypes at all for subroutines called as methods.

Remember, if your classes use inheritance, a method could be defined in more than one place in the inheritance tree and it's entirely possible that the prototype for each might be different. Since Perl doesn't know until runtime which subroutine implements a method for a specific object, it can't check the prototypes at compile time. So it doesn't.

4. OK, so prototypes are not perfect, but at least they pick up some classes of errors at compile time.

Perhaps, but used gratuitously, they can actually introduce some errors at runtime. Consider this subroutine:

sub border ($;$$) { my($width, $style, $colour) = @_; $style = 'solid' unless defined $style; $colour = 'black' unless defined $colour; return "border: $width $style $colour;"; }

In theory, the prototype declares that the subroutine expects one mandatory argument and two optional arguments (for which the routine defines default values). So if we call it like this:

print border('1px'), "\n";

it will return "border: 1px solid black;".

But if we call it like this:

my @args; push @args, $selected ? '5px' : '1px'; push @args, 'dashed'; push @args, 'red' if $selected; print border(@args), "\n";

Then if $selected is true, we might expect this return value:

"border: 5px dashed red;"

but we'd actually get:

"border: 3 solid black;"

The reason for this is that the prototype tells Perl we want the first argument as a scalar, so it evaluates @args in a scalar context which gives 3 (the number of elements in @args) and calls the subroutine with the lone argument (3) leaving the second and third arguments to default.

5. Well the coding standards for my project mandate the use of prototypes

That's a bug in the coding standards. It might be correct for other languages, but it's not correct for Perl.

6. My use of prototypes sends a clear message to people reading my code that I am a careful coder.

You're right, it does send a clear message - just not the one you think. People reading your code are pointing and sniggering as we speak.

7. Alright you win.

Actually, if you stop the gratuitous use of prototypes, we all win.

In truth, we never got to 7 the last time I had this discussion in real life :-)

Update

Since writing the above, I've encountered a coder who insists on using the perl4-style & prefix on subroutine calls - on the grounds that it clearly delineates subroutines from builtin functions (a questionable argument at best). Be warned that Perl does not check prototypes when a subroutine call includes the & prefix.

Replies are listed 'Best First'.
Re: Gratuitous use of Perl Prototypes
by BrowserUk (Patriarch) on Nov 09, 2004 at 01:41 UTC

    I completely agree with the {sentiment embodied in} the title of your meditation.

    However, I think that the word "gratuitous" is important. If prototypes were truely evil, as opposed to just widely misunderstood (Poor things. All say "Ahhh!"), then I think they would have been officially deprecated long ago.

    But they haven't. Maybe, because they do allow some things to be done that cannot be done any other way. And whilst you may question the necessity of doing those things, the desirability of them, when they are justified, is enough to keep them included in the langauge.

    Example: Remove the prototype from this implementation of zip and then modify the code to compensate and you get a much less readable and useable function.

    #! perl -slw use strict; local $, = ' | '; sub zip (&\@\@) { map { $_[ 0 ]->( $_[ 1 ][ $_ ], $_[ 2 ][ $_ ] ) } 0 .. ( @{$_[ 1 ]} < @{$_[ 2 ]} ? $#{ $_[ 1 ] } : $#{ $_[ 2 ] } ); } my @a = 'a' .. 'z'; my @n = 1 .. 10; print zip{ "@_" } @a, @n; print zip{ $_[ 0 ] .= $_[ 1 ] } @a, @n; print zip{ $_[ 0 ] .= $_[ 1 ] } @a, @n; __END__ [ 1:37:32.71] P:\test>406231 a 1 | b 2 | c 3 | d 4 | e 5 | f 6 | g 7 | h 8 | i 9 | j 10 a1 | b2 | c3 | d4 | e5 | f6 | g7 | h8 | i9 | j10 a11 | b22 | c33 | d44 | e55 | f66 | g77 | h88 | i99 | j1010

    Examine what is said, not who speaks.
    "Efficiency is intelligent laziness." -David Dunham
    "Think for yourself!" - Abigail
    "Memory, processor, disk in that order on the hardware side. Algorithm, algorithm, algorithm on the code side." - tachyon
      Example: Remove the prototype from this implementation of zip and then modify the code to compensate and you get a much less readable and useable function.

      I'd argue that zip() isn't that readable to begin with! *grins* Plus, can't you use some variant of reduce() or mapcar() to achieve the same thing?

      Also, isn't the subref prototype prone to memory leaks? I thought that's why we don't use the try/catch implementation in Error ...

      Being right, does not endow the right to be rude; politeness costs nothing.
      Being unknowing, is not the same as being stupid.
      Expressing a contrary opinion, whether to the individual or the group, is more often a sign of deeper thought than of cantankerous belligerence.
      Do not mistake your goals as the only goals; your opinion as the only opinion; your confidence as correctness. Saying you know better is not the same as explaining you know better.

        I'd argue that zip() isn't that readable to begin with! *grins*

        I guess I could rename it '¥' :)

        Or maybe you'd prefer map_one_element_from_each_of_two_arrays_at_a_time()?

        Or should that be map_one_element_at_a_time_from_each_of_two_arrays()?

        Plus, can't you use some variant of reduce() or mapcar() to achieve the same thing?

        Not that I am aware off. Anoyone know a better implementation?

        Also, isn't the subref prototype prone to memory leaks?

        Again. Not that I am aware of. Maybe the problem lies in the module?


        Examine what is said, not who speaks.
        "Efficiency is intelligent laziness." -David Dunham
        "Think for yourself!" - Abigail
        "Memory, processor, disk in that order on the hardware side. Algorithm, algorithm, algorithm on the code side." - tachyon
Re: Gratuitous use of Perl Prototypes
by ysth (Canon) on Nov 09, 2004 at 00:29 UTC
    sub PUSH {
    perlsub say:
    Subroutines whose names are in all upper case are reserved to the Perl core, as are modules whose names are in all lower case. A subroutine in all capitals is a loosely-held convention meaning it will be called indirectly by the run-time system itself, usually due to a triggered event. Subroutines that do special, pre-defined things include AUTOLOAD, CLONE, DESTROY plus all functions mentioned in perltie and PerlIO::via.
Re: Gratuitous use of Perl Prototypes
by perrin (Chancellor) on Nov 08, 2004 at 22:49 UTC
    My first thought any time I see code with prototypes is that the person who wrote it doesn't know Perl very well. There's no reason to use them except to try and do something tricky like your PUSH() example, and I think that's a bad idea because it has hidden behaviors and doesn't work the way other subs do. Better to treat all your subs consistently and use references explicitly when you want them.
      If you need particular types in your parameters, another way to achieve this (and quite possibly more robust than prototype as the clinical dissection above shows) is to use the "program by contract" paradigm.

      So if you need the first parameter to be an array ref, put in code to test this precondition -

      sub PUSH { my ($aref, @new) = @_; carp "first parameter must be an array reference" unless ref $aref e +q 'ARRAY'; ... }

      Even better, use Params::Validate

      If your emulating a builtin like push, using the same interface has advantages (in this case positional parameters), but I prefer using named parameters as much as possible - it also can cut out a number of errors if you choose appropriate parameter names e.g.

      sub NAMED_PUSH { my(%args) = @_; carp "dest_aref parmeter must be an array reference" unless ref $arg +s{dest_aref} eq 'ARRAY'; ... }

      Your POD for this sub (dont give me that puzzled look !!!) should document what keys are expected, and the code checks you have keep your part of the contract.

      use brain;

Re: Gratuitous use of Perl Prototypes
by talexb (Chancellor) on Nov 09, 2004 at 16:36 UTC

    Most illuminating. This would probably be a dynamite Lightning Talk -- pithy and very useful.

    It's particular useful to me, as a recycled C programmer going back to 1981 -- in C, good programming practice *demands* prototypes. Clearly, in Perl, that's not the case.

    Great post.

    Alex / talexb / Toronto

    "Groklaw is the open-source mentality applied to legal research" ~ Linus Torvalds

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://406231]
Approved by Arunbear
Front-paged by hsmyers
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others musing on the Monastery: (4)
As of 2024-04-18 22:58 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found