Beefy Boxes and Bandwidth Generously Provided by pair Networks
We don't bite newbies here... much
 
PerlMonks  

Spotting an empty array as argument

by Chuma (Scribe)
on Mar 25, 2021 at 20:00 UTC ( #11130334=perlquestion: print w/replies, xml ) Need Help??

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

I want to recreate the "say" function (that is, print but with a newline), but if given a list, it should print each item with a newline. If given no arguments, it should use $_. So I figure:

sub mysay{ if(@_>=1){ for(@_){print("$_\n")} }else{ print "$_\n"; } }

But now, if I happen to pass an empty array, like so...

@a=(); mysay @a;
...it will default to $_, when of course I'd like it to do nothing. The original say function apparently prints only a newline, which is not what I want, but implies that it should be possible to separate them.

So how can I distinguish between the two cases no argument, or an empty array?

Replies are listed 'Best First'.
Re: Spotting an empty array as argument
by choroba (Archbishop) on Mar 25, 2021 at 20:14 UTC
    To distinguish between no argument and an empty array, you can use the ;\@ prototype. It makes it impossible to specify more than one argument, though:
    #!/usr/bin/perl use warnings; use strict; use feature qw{ say }; sub mysay (;\@) { my ($arr) = @_; if (defined $arr) { print "$_\n" for @$arr; } else { print "$_\n"; } } my @arr2 = qw( Two/1 Two/2 ); mysay(@arr2); my @arr1 = qw( One ); mysay(@arr1); $_ = "Don't show"; my @arr0; mysay(@arr0); $_ = "Underscore"; mysay(); # This doesn't compile. # mysay(qw(a b c));
    map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]

      Neat! Although of course in this case I do need to specify more than one argument...

      > It makes it impossible to specify more than one argument, though:

      I think you are confusing @ with \@ . The former will swallow the whole list

      DB<40> sub my_say (;\@$$$) { dd \@_ } DB<41> my_say @a [["a1", "a2", "a3"]] DB<42> my_say @a,$a [["a1", "a2", "a3"], "A"] DB<43> sub tst (@) { dd @_ } DB<44> tst @a ("a1", "a2", "a3") DB<45> tst @a,2 ("a1", "a2", "a3", 2)

      Cheers Rolf
      (addicted to the Perl Programming Language :)
      Wikisyntax for the Monastery

        > I think you are confusing @ with \@.

        No, I mean what you can try by uncommenting the last line.

        map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
Re: Spotting an empty array as argument
by GrandFather (Saint) on Mar 25, 2021 at 20:15 UTC

    Perl "flattens" arrays and hashes passed as parameters to a sub into a list of scalar values (see the perlsub Description section, paragraph two. In your case it is not possible for the sub to tell the difference between a call with no parameters and a call containing one or more empty array and empty hash parameters.

    Perhaps a solution is to use a "sayArray" sub that does the multi-line thing and use Perl's "say" in other cases? That has the advantage that it clearly signals to the user that something special is happening in the array case.

    Perl's built in "say" doesn't need any magic for handling a "no argument" case. It just does what print does (in that case it prints nothing) then prints a newline, which is what it always does.

    Optimising for fewest key strokes only makes sense transmitting to Pluto or beyond

      Right, I suspected there might not be any nice way of doing it. I mean, "say" does seem to do some magic in the "no argument" case, just like "print", since it treats it differently from the "empty array" case. But maybe that's just a special rule for built-in functions.

      It's weird though, because there are so many built-in functions that have the same behaviour, defaulting to $_ if there are no arguments, and it's clearly useful, so it's surprising if there's no way of recreating that for your own functions.

        The prototype (_) can be used for the last argument. Some builtin functions can't be simulated, though, you can detect them by prototype('CORE::func') returning undef.

        For example, you can easily replicate the behaviour of uc, as prototype('CORE::uc') returns _. Similarly, pack has the prototype of $_ (the underscore must be the last one). But print or chomp return undef, so their behaviour is more complex and prototypes can't express it. So there is a special rule, but only for some of the built-in functions.

        map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
Re: Spotting an empty array as argument
by Discipulus (Abbot) on Mar 25, 2021 at 20:07 UTC
    Hello Chuma,

    > how can I distinguish between the two cases no argument, or an empty array?

    I bet you cant, because in a sub you receive arguments into @_ and no arguments is/means an empty array.

    Outside a sub it will be another story..

    L*

    There are no rules, there are no thumbs..
    Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.
Re: Spotting an empty array as argument
by LanX (Sage) on Mar 25, 2021 at 23:39 UTC
    > , but if given a list, it should print each item with a newline.

    maybe changing $, is just good enough for your task?

    DB<223> $,="\n" DB<224> @a=(); @b=1..3; $_=666 DB<225> say @b 1 2 3 DB<226> say @a DB<227> say 666 DB<228>

    But remember to use local, before changing global variables. :)

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    Wikisyntax for the Monastery

      Ha, there's certainly an easy solution! It doesn't address the more general problem, but for this particular purpose it does the job. Thanks!

      The only thing slightly off here is that "saying" an empty list still makes a newline. You'd think getting n newlines for n outputs shouldn't be that hard, but anyway, close enough.

Re: Spotting an empty array as argument
by LanX (Sage) on Mar 25, 2021 at 21:54 UTC
    There is a conceptual hole in Perl's arguments parsing, and choroba is spot on with his explanation.

    The only flexible workaround I found using prototypes is a (;&) , which will mean you need to put your parameters in a block.

    Not sure if you are willing to pay that price of surrounding your parameters in curlies, but a block can hold any number and type of list elements.

    DB<32> sub my_say (;&) { $_[0] ? say $_[0]->() : say $_ } DB<33> @a=('a1'..'a3'); $a="A",$b="B"; $_=666 DB<34> my_say 666 DB<35> my_say {@a} a1a2a3 DB<36> my_say {@a,$a} a1a2a3A DB<37> my_say {$a,1..3} A123 DB<38>

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    Wikisyntax for the Monastery

      That's probably too cumbersome in this case, considering the only advantage I'm striving for is being able to write "say;" instead of "say $_;". But nice to know anyway!

Re: Spotting an empty array as argument
by ikegami (Pope) on Mar 26, 2021 at 19:23 UTC

    You can't pass arrays (or hashes) to subs, only a sequence of zero or more scalars. There is no difference between

    f(@a)
    and
    f($a[0], $a[1], $a[2], ...)

    so you can't distinguish

    my @empty; f(@empty)
    from
    f()

    At best, you can use prototypes to change what scalars are passed to a sub.

    sub f(;\@)
    will allow
    f(@a) # Calls &f(\@a)
    and
    f() # Calls &f()
    but not
    f($x, $y)

    What you want to achieve, however, can't be achieved using prototypes. You'd have to use something like Devel::CallParser.

    Seeking work! You can reach me at ikegami@adaelis.com

Re: Spotting an empty array as argument
by haukex (Bishop) on Mar 26, 2021 at 22:15 UTC

    Here's something that AFAICT meets your requirements, but since it's Friday evening I might be missing a test case for which it fails*. But as I mention here, it's really still just an approximation.

    sub mysay (;+@) { return say $_ unless @_; if ( ref $_[0] eq 'ARRAY' ) { unshift @_, @{shift()} } elsif ( ref $_[0] eq 'HASH' ) { unshift @_, %{shift()} } say join "\n", @_; }

    * Update: Of course, just two minutes after posting I thought of one: say \@x would normally print ARRAY(0x...), this function dereferences it. Like I said, an approximation! I think you might be better off just biting the bullet and writing say join "\n", ...; ...

      good try :) But ...

      DB<262> sub mysay (;+@) { print @_ } DB<263> mysay 1,2,3 123 DB<264> mysay 1..3 DB<265> mysay 1..3,4..6 456 DB<268>

      It's enforcing scalar context on the first argument, and this is one of the unfortunate cases where an operator is changing it's meaning on context. :(

      Cheers Rolf
      (addicted to the Perl Programming Language :)
      Wikisyntax for the Monastery

        mysay 1..3

        Ah, a very good point, thanks!

        Chuma: This once again underlines the fact that, unfortunately (!), you're probably not going to get everything you want here. Perhaps it would also make sense to take a step back and explain what you want to use this function for? And if you think about it again, do you really need to default to $_? Or could you split this into two different functions? etc.

Re: Spotting an empty array as argument
by tobyink (Canon) on Mar 26, 2021 at 10:11 UTC
      Try my test here.

      map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
Re: Spotting an empty array as argument
by Chuma (Scribe) on Mar 26, 2021 at 13:43 UTC

    Well, there is of course a way of doing it, but it's not pretty...

    sub brutesay{ if(@_>=1){ for(@_){print("$_\n")} }else{ ($package,$filename,$line) = caller; open $fh,$filename; for $i(0..$line-2){<$fh>} if(<$fh>=~/brutesay *(\(\))? *;/){print "$_\n";} } }

      That'll break if you have two brutesays on one line ;-) print and say get special parsing by Perl, and as others have explained, you can't replicate their behavior 100% with Prototypes, everything will just be an approximation. I think the closest you'll get functionally is LanX's $, suggestion.

      Well yes, I was reluctant to suggesting parsing the source since it seemed far beyond your scope. (Caller has also some limitations if it comes to the line number)

      IIRC is Carp listing the stack trace with the original arguments if called inside the debugger. That's because DB is caching all source lines internally.

      You could do the same with a "passiv" source filter.

      Since it would only read and not change the source it can't possibly cause any harm.

      Another way is adding __DATA__ at the end of your file, because the DATA filehandle is just reading your source. ( seek and see ;)

      Both possibilities need far less resources and are more reliable.

      Cheers Rolf
      (addicted to the Perl Programming Language :)
      Wikisyntax for the Monastery

Re: Spotting an empty array as argument (updated)
by LanX (Sage) on Mar 25, 2021 at 22:50 UTC
    There might be a possibility tho, but it's a bit hackish ...

    a mysay(@a) or mysay($a) will alias $_[0] to $a_[0] or $a resp. and so on

    But $_[0] won't be aliased if there are no parameters.

    So if there is an internal way to check this, you've won.

    Unfortunately I'm to busy to check for this and I suppose you don't wanna dive into that amount of magic anyway ... :)

    update

    Sorry, never mind.

    This won't help you distinguishing between no parameters and an empty @a.

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    Wikisyntax for the Monastery

Re: Spotting an empty array as argument
by LanX (Sage) on Mar 26, 2021 at 17:47 UTC
    Another idea:

    Since one can change the output handle with select , it should also be possible to change the print method of the IO object.

    In a perfect world are print and say only calling that method.

    But who knows what kind of magic rules here and I'm too tired to check this out now. :)

    update

    See also tied file handles:

    A class implementing a filehandle should have the following methods: TIEHANDLE classname, LIST READ this, scalar, length, offset READLINE this GETC this WRITE this, scalar, length, offset PRINT this, LIST <---- PRINTF this, format, LIST BINMODE this EOF this FILENO this SEEK this, position, whence TELL this OPEN this, mode, LIST CLOSE this DESTROY this UNTIE this

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    Wikisyntax for the Monastery

        Awesome, an expert! =)

        So, is it feasible?

        update

        Looks good to me, even with core modules. =)

        use v5.12; # enable say & strict use warnings; { package MySay; use Data::Dump qw/pp dd/; use Carp; require Tie::Handle; our @ISA = qw(Tie::Handle); sub TIEHANDLE { #carp pp '\@_: ', \@_; bless \ my $i, shift } sub PRINT { my $self = shift; print STDOUT "$_" for @_ } sub new { my $self = shift; open my $fh, ">&STDOUT"; tie *$fh, 'MySay'; return($fh); } # sub DESTROY { # print STDOUT "DESTROY"; # # my $self = shift; # select(STDOUT); # } } { package main; # ---- test vars $_='$_'; my @a = map { "\$a[$_]" } 0..2; my @b; { select MySay->new(); say; say @b; say @a; select(STDOUT) } say "AF","TER" }
        C:/Strawberry/perl/bin\perl.exe -w d:/tmp/pm/tie_handle.pl $_ $a[0] $a[1] $a[2] AFTER

        with some effort it might even be possible to automatically destroy the selection at end of scope, such that one only needs to write MySay->select() once without any further need for visible select

        But I got tired and wanted to get it done. :)

        Cheers Rolf
        (addicted to the Perl Programming Language :)
        Wikisyntax for the Monastery

A reply falls below the community's threshold of quality. You may see it by logging in.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://11130334]
Approved by GrandFather
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others taking refuge in the Monastery: (4)
As of 2021-12-05 14:35 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    R or B?



    Results (31 votes). Check out past polls.

    Notices?