5

I encountered this today and thought it prudent to post a Q&A as I couldn't find anything similar.
Feel free to vote-to-close if you find a duplicate of this question.


The following subroutine conditionally returns output; I consider it "clumsy" because it isn't explicit about what is returned to the caller when the conditional is not satisfied:

sub is_multiple_of_three {

    my ( $value ) = @_ ;
    return "$value is a multiple of 3"
      unless $value % 3;
}

A quick rewrite makes short work of clarifying the (more graceful) subroutine's intended behaviour under all circumstances:

sub is_multiple_of_three { 

    my ( $value ) = @_ ;
    return if $value % 3;
    return "$value is a multiple of 3";
}

When calling these both flavours of the subroutine, I was expecting to find some consistency between what both return in list context:

  • a string when the conditional evaluates to true
  • nothing (an empty list) when the conditional evaluates to false

But alas, the behaviour was slightly unexpected:

use strict;
use warnings;
use Data::Printer;
use feature 'say';

my %subs = (
            graceful => sub {
                            my ( $value ) = @_ ;
                            return if $value % 3;
                            return "$value is a multiple of 3";
                        },

              clumsy => sub {
                            my ( $value ) = @_ ;
                            return "$value is a multiple of 3"
                              unless $value % 3;
                        },
           );

for my $name ( keys %subs ) {

    my $sub = $subs{$name};
    say $name;
    my @results = map { $sub->($_) } 1 .. 10;
    p @results;
}

Output

graceful
[
    [0] "3 is a multiple of 3",
    [1] "6 is a multiple of 3",
    [2] "9 is a multiple of 3"
]
clumsy
[
    [0] 1,
    [1] 2,
    [2] "3 is a multiple of 3",
    [3] 1,
    [4] 2,
    [5] "6 is a multiple of 3",
    [6] 1,
    [7] 2,
    [8] "9 is a multiple of 3",
    [9] 1
]

Question

The "graceful" flavour behaves as expected, but why is the "clumsy" sub returning back integers when the conditional is false?

Zaid
  • 36,680
  • 16
  • 86
  • 155
  • Related [Should a subroutine always return explicitly?](https://stackoverflow.com/questions/28761824/should-a-subroutine-always-return-explicitly). I am not sure if this should be considered a duplicate. – Sinan Ünür Jul 17 '17 at 21:47

2 Answers2

7

The behaviour is consistent with what is documented in perldoc perlsub

A return statement may be used to exit a subroutine, optionally specifying the returned value, which will be evaluated in the appropriate context (list, scalar, or void) depending on the context of the subroutine call. If you specify no return value, the subroutine returns an empty list in list context, the undefined value in scalar context, or nothing in void context. If you return one or more aggregates (arrays and hashes), these will be flattened together into one large indistinguishable list.

If no return is found and if the last statement is an expression, its value is returned. If the last statement is a loop control structure like a foreach or a while , the returned value is unspecified. The empty sub returns the empty list.


The graceful sub in list context:

  • True : returns the string "$value is a multiple of 3" is returned

  • False : returns an empty list

Which is why there are only three elements in @results; something is added to the array only when the conditional evaluates to true.

The clumsy sub in list context:

  • True : returns the string "$value is a multiple of 3" is returned. No dramas here.
  • False : as no explicit return is encountered, returns the value of the last expression evaluated , $value % 3

So in both cases, the subroutine will return back a value, which is why @results has ten items in it.

Zaid
  • 36,680
  • 16
  • 86
  • 155
  • Here's a rub for me -- the last "thing" in "clumsy" is `if (not ...)`. The docs specify behavior for either an "expression" or for "loop control structure" ... so what about `if`, or `not`? It appears that they "don't count", it still returns the last evaluation. However, this may also happen under "unspecified" -- we get the value here, but in some other case it may be something else. – zdim Jul 17 '17 at 21:51
  • See [Statement Modifiers](http://perldoc.perl.org/perlsyn.html#Simple-Statements) in `perlsyn` for `EXPR` examples – Zaid Jul 17 '17 at 21:53
  • I am asking about what comes _after_ the expression is evaluated -- it's the `unless` that is executed (which should be `not` followed by `if`). The docs don't seem to specify what happens when the last thing is an `if`. – zdim Jul 17 '17 at 21:59
  • @zdim Let me repost the comment here: Interestingly, changing the clumsy sub to `return if not $value % 3` results in the empty string being returned, not the result of the modulus – Zaid Jul 17 '17 at 22:20
  • Whoa, now that's a discovery (to me). So `unless` behaves differently from `if not`, in a way that has consequences. I don't mean the syntactic differences (one can't do `EXPR for my $var LIST` for example), but the order of processing. It seems from your example that the last thing that is done in `unless EXPR` is in fact the evaluation of the `EXPR`, while in `if (! EXPR)` it seems to be `if` – zdim Jul 17 '17 at 22:22
  • @zdim yes, it would seem so. It would make for a pretty interesting Q&A I guess. The difference is that the `not` is considered to be part of `EXPR`, while in `unless` it isn't – Zaid Jul 17 '17 at 22:23
  • Good point, but still -- then `unless` is only fully equivalent to `if`, and so they are processed differently. That's consequential. – zdim Jul 17 '17 at 22:24
  • @zdim should I post the question or will you do it? – Zaid Jul 17 '17 at 22:26
  • Thanks for asking -- please go ahead and post. – zdim Jul 17 '17 at 22:28
  • Thank you for the note, I just came back to my computer. Will pitch in as soon as I get a minute, I think it's a very interesting question. – zdim Jul 18 '17 at 01:37
  • It was my second comment with that _wrong_ statement "_`unless` ... should be `not` followed by `if`_" that triggered the discussion that lead to the other question. Sorry that I pushed the wrong way. I still think that it was a good question to ask. Btw, I also think that my _first_ comment here stands -- the docs indeed don't specify how `if` weighs in this context. (Probably doesn't matter a bit -- apparently, it "doesn't count" so to say, what makes sense.) – zdim Jul 18 '17 at 23:02
  • I came all the way for this *If no return is found and if the last statement is an expression, its value is returned* Thats weird and unexpected! – Jalal Dec 27 '21 at 13:12
2

For last-expression purposes, foo if bar and if (bar) { foo } are equivalent to bar and foo, and similarly unless is equivalent to or.

ysth
  • 96,171
  • 6
  • 121
  • 214