Dr. Mu has asked for the wisdom of the Perl Monks concerning the following question:

I'm trying to write a universal accessor function called link. Its purpose is to access one of an object's many links to another object or collection of objects. When a link refers to a collection of objects, that collection is stored as an array reference, viz:
$self->{link}->{$linkname} = [@objectrefs]
Otherwise, the object reference of the linked-to object is simply stored as a scalar.

When the link function is called in list context, I want it to return a list of the linked-to objects, regardless of whether there is just one or many. When it's called in scalar context, I want it to return either a single object if there's only one, or the array reference if there's more than one. Here's what I wrote:

sub link { my $self = shift; my $linkname = shift; my $link = $self->{link}->{$linkname}; return wantarray ? (ref $link ? @$link : ($link)) : $link }
All fine and good. But here's where I get into trouble. There are times when I just want to find out the size of the named link. My first inclination was to use something like scalar $obj->link('next'), for example. Obviously, if the subroutine is returning an argument to scalar, I'm wanting it to return a list so I can measure its size. Right?

Well, no, apparently. Rather than acting like a real function, scalar is forcing the context before the subroutine call, as wantarray returns false. So my question is, how can I call link in list context and force the result into scalar context -- without being too ugly about it? (And, yes, I know link is a keyword. I should probably change the sub name to "linkto" or somesuch.)

Replies are listed 'Best First'.
Re: Confused Contexts and wantarray
by Zaxo (Archbishop) on Jan 18, 2005 at 01:40 UTC
    When it's called in scalar context, I want it to return either a single object if there's only one, or the array reference if there's more than one

    In my opinion, that's the wrong thing to do. wantarray lets you take context from what the caller wants. It amounts to an extra service by the sub. Changing the return type based on some property of the data is a whole different thing. It requires the caller to either test or already know what it gets back from the call. I consider that a design error.

    The link data should be designed to always be the same kind of thing, in this case an array reference. That will save a lot of trouble.

    After Compline,
    Zaxo

      return either a single object if there's only one, or the array reference if there's more than one
      In my opinion, that's the wrong thing to do. ... Changing the return type based on some property of the data is a whole different thing. It requires the caller to either test or already know what it gets back from the call. I consider that a design error.

      I completely agree with that. It's one of the things that's so annoying about CGI::Lite (for which I'm the current maintainer); having to test whether you've got an array-ref and then deal with it if you have is more effort than just always getting an array-ref.

      The link data should be designed to always be the same kind of thing, in this case an array reference.

      I'm not convinced by that, though. I like what CGI does in scalar context: it presumes that you used scalar context cos you're only expecting one item, so it just gives you one item, the first (or only) item in the list. I think that approach may also make sense here:

      return ref $link ? (wantarray ? @$link : $link->[0]) : $link;
      Smylers
Re: Confused Contexts and wantarray
by Errto (Vicar) on Jan 18, 2005 at 01:44 UTC
    I'm wanting it to return a list so I can measure its size.

    Not quite. The best thing I like to say to start out with in these discussions is "there is no such thing as a list in scalar context." The result you're going for is that an array in scalar context returns its number of elements. And it is precisely the purpose of the function "scalar" to apply scalar context to the expression it's being called on. No before or after about it. But I should think that something like

    scalar @{[ $obj->link('next') ]}
    ought to work. There's a few things going on here. The inner brackets do two things: a) force the link method to be called in list context, and b) form an anonymous array out of the results. But the return value of the brackets is just a reference to the array. What you really need is the array itself in scalar context, as if it were a "real" array variable with a name and everything. That's what the @ sigil and the outer braces are doing. They're taking this array reference you just created and de-referencing it as a "real" array which, when invoked in scalar context, returns its size. The reason that
    scalar @{ $obj->link('next') }
    doesn't work is that the expression inside the braces is evaluated in scalar context, and the result is expected to be either an array reference or an identifier (in which case it becomes a symbolic rerefence to the array of that name in the current package). Fun, huh?

    Update: Of course, if what the method does in scalar context is return an array reference, then the second construct will work fine also.

Re: Confused Contexts and wantarray
by Tanktalus (Canon) on Jan 18, 2005 at 01:26 UTC

    A little experimentation got me the following (somewhat scary) result:

    use strict; sub flab { my @r = ( 1 .. 10 ); wantarray ? @r : \@r; } print "flab has ", scalar @{flab()}, " entries\n";
    It does work ...

      Hi folks, New to 'monks but not Perl. I've used it (wantarray) very effectively (>5.6.1, probably well before) in this way.
      sub func { my(...) = @_; my @new_a; # assume some assignment to @new_a based on params for(....) {} wantarray? @new_a: \@new_a; } # this one gets you a reference to @new_a in &func() # which is available as long as $ar_array is in scope my $ar_array = &func(...); # this is the copy of @new_a from &func() # @new_a is gone my @array = &func(...); Also: wantarray? @foo: "@foo" wantarray? @foo: $foo[0] aren't nearly the same except the first above is an implicit join to scalar and the second is the first element which may or may not be what you want.
      Please contact me: morourke@theworld.com

      how is this scary?

      @{...} puts flab() in array context so it returns the array @r.

      scalar(...) then evaluates that array in scalar context, returning its size.

      I don't see anything magical in there. (apart from me probably mixing array and lists again)

Re: Confused Contexts and wantarray
by Dr. Mu (Hermit) on Jan 18, 2005 at 05:28 UTC
    Thanks for the comments. I'll grant that the desired operation of link is somewhat ill-conceived. And I'll probably change it. But once I discovered the contextual issues, I couldn't just let them go. Perhaps the most readable approach would be to add a size routine:
    my $n = size($obj->link('next')); sub size {scalar @_}
    The routine doesn't do a thing except force a list context on link and scalarize its return value. But having it there also makes clear what's intended.

    <heretical rant>
    In my mind size ought to have been a built-in function, rather than relying solely on implied semantics and contextual hocus-pocus to do its job. The number of newbies who try to use length for this purpose is, by itself, testament to an unmet need.
    </heretical rant>

    And just think of all the questions in SoPW that might've been eliminated!

      In my mind size ought to have been a built-in function, rather than relying solely on implied semantics and contextual hocus-pocus to do its job.

      You'll get your wish in perl6 as there will be a method you can use on array objects to tell you how many items are in the array. Last I looked it was called nelems but that may have changed.

      As an aside, length is going the way of the dinosaur too. It'll be replaced by routines that are more specific and will give you the number of bytes, characters, graphemes, etc. that are contained within a scalar.