Beefy Boxes and Bandwidth Generously Provided by pair Networks
Think about Loose Coupling
 
PerlMonks  

Thoughts on "one function, flexible arguments"?

by hornpipe2 (Sexton)
on Aug 28, 2019 at 22:44 UTC ( #11105190=perlquestion: print w/replies, xml ) Need Help??

hornpipe2 has asked for the wisdom of the Perl Monks concerning the following question:

Suppose I have a subroutine which expects a hash reference, containing named parameters. The simple way to write this is:
sub my_func { my $params = shift; print "Message received: " . $params->{message}; if ($params->{newline}) { print "\n" } } # good my_func( { message => "Hello, world!" } ); # "Error: Not a Hash reference" my_func( message => "Hello, world!" );
But there is a more "liberal" way to write this, which accepts the parameters in a variety of presentations (list, list-ified hash, scalar for a single "most important" param, etc):
sub my_func { my %params; if (ref($_[0]) eq 'HASH') { %params = %{+shift}; } elsif (ref($_[0]) eq 'ARRAY') { (%params) = @{+shift}; } elsif (ref($_[0]) eq 'SCALAR') { $params{message} = ${+shift}; } elsif (scalar @_ > 1) { (%params) = @_; } else { $params{message} = shift; } print "Message received: " . $params{message}; if ($params{newline}) { print "\n" } } # good my_func( { message => "Hello, world!" } ); # also good my_func( message => "Hello, world!" );
What are the community's thoughts on this pattern? Is this easier for users, and is the trade-off worthwhile for the maintainer?

Replies are listed 'Best First'.
Re: Thoughts on "one function, flexible arguments"?
by davido (Cardinal) on Aug 29, 2019 at 07:15 UTC

    While at the outset it would seem that this adheres to Postel's law ("Be liberal in what you accept, and conservative in what you send.", paraphrased.), it's a bit of a path toward madness. The more alternatives you allow, the more you are agreeing to support all those alternatives forever, for some values of forever. You are also adding complexity to your code, making param unpacking more difficult, particularly in special cases, and are painting yourself into a corner where the calling alternatives may prevent you from doing something with the args that makes perfect sense in a specific case, but differs from the myriad of ways all of the other methods in your library work.

    I suggest either requiring a hashref, or permitting either a hashref or key/value pairs. Don't go providing coercing of array-refs into hash-refs, or scalar refs, or code-refs (executed to produce the args)... unless you have a good reason for providing one of those, and in that case don't provide all the other ways. It's really an overzealous application of the perceived spirit of Postel's law, but isn't founded in the actual intent.

    Also if you do find yourself doing this often, consider putting the args unpacking into a subroutine or method that can be called for every sub with conforming arg handling.

    If you really need to provide significantly different ways to pass args, consider writing one version of the method, and then creating wrapper methods that provide the alternate calling styles:

    sub my_func { my $args = ref($_[0]) eq 'HASH' ? shift : {@_}; # do the thing } sub my_func_positional { return my_func({ message => shift(), (@_ ? (newline => shift) : ()), }); }

    There is a module, Params::Smart which will handle both positional and key/value args, but the time I used it I kind of wish I had just created two subs to call instead.


    Dave

      davido:

      I just wanted to chime in on Postel's law. As I see it, much of the difficulty of the WWW comes from allowing the browser to ignore malformed HTML without complaint. I agree that the browser could try to make a good attempt at rendering broken HTML, but it should *never* do it silently. The problems weren't that the browser tried to render garbage, but that (a) it would do so without complaint*, and (b) that it would try far too hard** to make it sensible.

      If web developers had to deal with the alert box every time a page load had an error, they'd either fix the problem because they got sick of seeing it, or they'd be forced to fix it when the people signing their paychecks saw it and said "WTF!?".

      Notes:
      * Back in the day, you'd get an alert box, but it had a checkbox at the bottom that said something to the effect of "I never want to see these warnings again", and people would click that and then never again see the alert box. It lead to a culture of "Meh, it works well enough, I guess I'm done" and crap coding. It contributed to the perception that JavaScript is a toy language in a toy environment, and lead to a bunch of slackadaisical front end developers.
      ** If we had to ensure that we provided correct HTML, there would be far less effort involved in "bug-compatible" HTML rendering tweaks on varied browsers. They could try to do something sensible so that developers could make changes to their pages and see what would happen, knowing that the errors would get fixed before release, so it wouldn't really matter if the table was rendered entirely in italics or whatever.

      ...roboticus

      When your only tool is a hammer, all problems look like your thumb.

Re: Thoughts on "one function, flexible arguments"?
by NetWallah (Canon) on Aug 28, 2019 at 23:40 UTC
    Possibly easy to maintain AND flexible parameters:
    use strict; use warnings; sub my_func { my %params; my $fix_dispatcher = { HASH => sub{ %params = %{$_[0]}}, ARRAY => sub{ %params = @{$_[0]}}, SCALAR => sub{ $params{message} = ${$_[0]}}, _DIRECT_SCALAR=> sub{ $params{message} = $_[0]}, _DIRECT_ARRAY => sub{ %params = @_}, }; my $param_type = scalar(@_)> 1 ? '_DIRECT_ARRAY' : ref $_[0] || '_DIRECT_SCALAR'; $fix_dispatcher->{$param_type}->(@_); print "Message received: " . $params{message}; if ($params{newline}) { print "\n" } } # good my_func( { message => "Hello, world! (hashref)", newline=>1 } ); # also good my_func( message => "Hello, world! (hash)" ,newline=>1); my_func( "Hello, world! (Scalar)" ); print("\n"); my_func(\"Hello, world! (Scalar Ref)" ); print("\n"); my_func( "message" , "Hello, world! (array)", "newline",1 ); my_func( ["message" , "Hello, world! (array ref)", "newline",1 ]);
    UPDATE1: Added "array ref" call.
    UPDATE2: Deleted unnecessary variable "$fix_subref".

                    "From there to here, from here to there, funny things are everywhere." -- Dr. Seuss

Re: Thoughts on "one function, flexible arguments"?
by GrandFather (Sage) on Aug 29, 2019 at 07:52 UTC

    A useful test for any API (and passing parameters to a sub is an API in miniture) is: how easy is it to test against? If the test need to be complicated to accommodate the API it implies that the API is complicated and probably difficult both to use and implement. In the longer term that makes it error prone to use and hard to maintain.

    Focus your effort on creating the simplest consistent API that achieves the task at hand. If you can't create a simple consistent API that probably means the task at hand is too complicated or badly conceived.

    Optimising for fewest key strokes only makes sense transmitting to Pluto or beyond
Re: Thoughts on "one function, flexible arguments"?
by jcb (Vicar) on Aug 28, 2019 at 23:33 UTC

    I believe that this is taking TIMTOWDI a bit too far.

    The most common form I have seen is a list of key-value pairs as the argument list. Using a hashref is generally done only in functions that have been retrofitted to accept keyword options where a hashref was previously forbidden in the argument list.

    sub my_func { my %params = @_; print "Message received: " . $params->{message}; if ($params->{newline}) { print "\n" } }
      I did some further research on Perlmonks, and found that there are pros and cons to accepting hashref vs listified hash. For example if you make an error in building your hash: one dies at compile-time, the other at run-time.

      I also went out to MetaCPAN and checked the most popular modules list. Using this (highly unscientific) method I determined that most module authors (LWP::UserAgent, DateTime) have gone with listified hash, instead of hashref. So, that is what I'll stick to as well.

Re: Thoughts on "one function, flexible arguments"?
by roboticus (Chancellor) on Aug 29, 2019 at 12:51 UTC

    hornpipe2:

    If you feel compelled to give it a go, you might try Kevorkian* as daxim mentioned, or Params::Smart that davido suggested, as one of these could remove a lot of the complexity from your functions. (I've never used them but may try 'em out of curiosity.)

    Having said that, though, I'm going ring in with agreement with GrandFather and davido on this one: While having some flexibility in the arguments can be a good thing, it's easy to go too far. I've seen issues where (a) you get few/no users of one calling method, (b) extra difficulty in sussing out what change(s) you may need to make when you take all the calling conventions into account, (c) maintaining of code that's never even used, (d) the flexibility adds rather than removes confusion to users.

    (*) Sorry for that, but it's the first thing that came to mind when I saw daxim's response.

    ...roboticus

    When your only tool is a hammer, all problems look like your thumb.

Re: Thoughts on "one function, flexible arguments"?
by 1nickt (Abbot) on Aug 28, 2019 at 23:34 UTC

    Hi, I would say bad idea because not easier for users as they then have to keep track of which functions they use are liberal and which are not, and definitely not easier for maintainers.

    FWIW I like Method::Signatures a lot for sub signatures and basic param validation, and it has the advantage that you can document the function with the sdame signature you use with it, and the user knows what the API is.

    Hope this helps!


    The way forward always starts with a minimal test.
Re: Thoughts on "one function, flexible arguments"?
by daxim (Curate) on Aug 29, 2019 at 07:03 UTC
    Use Kavorka (and Moops in case of object-oriented programming) for flexible calling-style (both plain key/value and hashref) and sensible error messages and type constraints. It's the best of the bunch. my_func only contains logic - look at all the boilerplate code you did not have to write!

    Positional parameters and named parameters may be mixed, but each parameter cannot be both. It is not a good idea because that would preclude optional parameters, hence this is not supported.

    use Kavorka; fun my_func(Str :$message!, Bool :$newline) { print "Message received: $message"; print "\n" if $newline; } my_func(message => 'x'); my_func(message => 'y', newline => 1); my_func({message => 'z'}); my_func(); # Named parameter `message` is required my_func(message => do { require DateTime; DateTime->now }); # Reference bless( {...) did not pass type constraint "Str"
Re: Thoughts on "one function, flexible arguments"?
by LanX (Cardinal) on Aug 31, 2019 at 02:13 UTC
    The real issue is kind of semipredicate problem, which is avoided in your special example but can't be in general.

    So what happens if the first positional argument is a hash ref and not a string like in your case?

    How can you tell that this ref doesn't represent a hash of named arguments?

    If this happens you'd need to diverge from your interface standard and will confuse yourself and your users.

    Better stick with a convention which doesn't need special cases.

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    Wikisyntax for the Monastery FootballPerl is like chess, only without the dice

Re: Thoughts on "one function, flexible arguments"?
by Anonymous Monk on Aug 30, 2019 at 20:08 UTC
    I do not agree with Postel: "be conservative" in what you accept, too. Otherwise you have just made a too-generalized promise to anyone who shows up that, somehow, you will do the right thing with anything they offer. My point being that I could never "desk check" YOUR code, unless I could somehow comb the universe to discover each and every call. The "correct" operation of this code as written depends on the "correctness" of every possible caller and could offer no assistance at all in detecting that a bug existed in any of them. Of course, "realizing that a bug exists" is always the first hurdle, and "determining where it is" is the second. Maybe i would rewrite Postel's rule as: "be suspicious as hell, because if you don't catch the CALLER'S bug, no one will."

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://11105190]
Approved by johngg
Front-paged by footpad
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others romping around the Monastery: (3)
As of 2020-12-04 04:49 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    How often do you use taint mode?





    Results (58 votes). Check out past polls.

    Notices?