6

I have this bit of code. The first, non-nested map outputs some stuff, and the nested one doesn't. I think I understand why the second doesn't work. It's a lazy sequence and Perl 6 is gathering the results. That's fine. But isn't the first (non-nested) map lazy in the same way? How does it output anything if I don't do anything with the result of the map? That is, how is the first one lazy? Does is automatically get a sink context where I have to explicitly supply sink (or something) to the nested one? Somehow I think Perl 6 should be able to figure this out for me.

my @array = (1, 2), (3, 4), ('a', 'b');

say "---On its own:";
my @item = 1, 2, 3;
@item.map: {
    say $_;
    };

say "---Inside another map:";
@array.map: {
    my @item = 1, 2, 3;
    @item.map: {
        say $_;
        }
    };

Here's the output:

---On its own:
1
2
3
---Inside another map:

This is related to the question How can I use "map" inside a "for" loop in Perl 6?. That one says what to do, but I'm asking more of a why question.

In that, the solution was to add eager, sink, or assignment to the interior map:

say "---eager:";
@array.map: {
    my @item = 1, 2, 3;
    eager @item.map: {
        say $_;
        }
    };

say "---sink:";
@array.map: {
    my @item = 1, 2, 3;
    sink @item.map: {
        say $_;
        }
    };

say "---assignment:";
@array.map: {
    my @item = 1, 2, 3;
    @ = @item.map: {
        say $_;
        }
    };
Community
  • 1
  • 1
brian d foy
  • 129,424
  • 31
  • 207
  • 592
  • *Somehow I think Perl 6 should be able to figure this out for me.* Returning a `Seq` is a valid use case. As compilers can't read minds, what kind of regular semantics would you prefer? Make sink context recursive, iterating all sub-sequences of a sequence in sink context? To me, that looks like a can of worms that probably should stay closed – Christoph Jan 17 '17 at 14:33
  • I'd much rather have a map like thing that returns a list. The idea that it's a sequence instead of an ordered list seems odd to me. – brian d foy Jan 17 '17 at 14:54
  • Looking back, it also took me a while to get comfortable with sequences: in theory, they are the better primitive (eg you can easily make sequences into lists, but not the other way around), but in practice there was some friction. This particular problem is less about sequences vs lists, though, but laziness... – Christoph Jan 17 '17 at 15:29
  • 1
    So you want `map { $_+1 }, 1..*` or `[\+] 1..*` to return a list? If you only need the first few elements of the Seq, it will only produce the first few rather than waste a bunch of time. `say [\+](1,3...*)[^20]` Seqs can also throw away the values after they have been used in iteration (if you don't do anything to cache them). There was a time that List did both jobs, and it was far harder to reason about even for the implementers. – Brad Gilbert Jan 17 '17 at 16:16
  • I don't want anything in particular really. It's more how much a beginner has to know before they can use something. Here they have to understand quite a bit, so they are going to make these sorts of mistakes. – brian d foy Jan 17 '17 at 18:39

2 Answers2

9

The content of every program file, block, subroutine, etc. is a "statement list" consisting of semicolon-separated1 statements. With that in mind:

  1. Statements that get sunk:

    • All but the final statement in a statement list.
    • Any loop statement (for, while, etc.2) at statement-list level3, even when it is the final one.
    • Any statement in the top-level statement list of a program or module file, even when it is the final one.4
  2. Statements that get returned instead of sunk:

    • The final statement in a statement list, except for the cases mentioned above.

Sinking forces eager evaluation, returning doesn't.

Example

In your case, the first map statement is in the middle of a statement list, so it gets sunk.

But the nested map statement is the final statement of its statement list, so its result gets returned in the form of a not-yet-iterated Seq.

Its parent map statement is also a final statement, but it's in the top-level statement list of the program file, so it gets sunk, causing it to eagerly iterate the sequence that consists of three Seq values. (Insert a say statement before the inner map, to see this.)
But nothing sinks, or otherwise iterates, each of those three inner Seq values.

From the design docs

More verbosely, from Synopsis 04, line 664:

In any sequence of statements, only the value of the final statement is returned, so all prior statements are evaluated in sink context, which is automatically eager, to force the evaluation of side effects. (Side effects are the only reason to execute such statements in the first place, and Perl will, in fact, warn you if you do something that is "useless" in sink context.) A loop in sink context not only evaluates itself eagerly, but can optimize away the production of any values from the loop.

The final statement of a statement list is not a sink context, and can return any value including a lazy list. However, to support the expectations of imperative programmers (the vast majority of us, it turns out), any explicit loop found as the final statement of a statement list is automatically forced to use sink semantics so that the loop executes to completion before returning from the block.

This forced sink context is applied to loops only at the statement list level, that is, at the top level of a compilation unit, or directly inside a block. Constructs that parse a single statement or semilist as an argument are presumed to want the results of that statement, so such constructs remain lazy even when that statement is a loop.


1) When a closing brace } appears as the last proper token of a line, as in
my @a = @b.map: { $_ + 1 } # whitespace/comment doesn't count
it also ends the current statement, but otherwise a semicolon is needed to separate statements.

2) map doesn't count, because it is a function and not a loop keyword.

3) Meaning that when a loop statement appears in a different place than directly in a statement list, e.g.
lazy for ^10 { .say } # as argument to a keyword expecting a single statement
(for ^10 { .say }) # inside an expression
then it isn't sunk by default. That's what the last paragraph of the synopsis quote is trying to say.

UPDATE: This doesn't seem to actually the case in Rakudo, but that may be a bug.

4) This rule isn't mentioned in the synopsis, but it's how it works in Rakudo, and I'm pretty sure it's intentional.

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
smls
  • 5,738
  • 24
  • 29
6

.map basically returns a .Seq. What happens is that the inner map returns a Seq to the outer map, but since the results of that map are sunk, they disappear without being iterated on.

If you say the outer map, you will pull the result of the inner map, and you will see the result of the .Seq the inner map returned:

my @array = (1, 2), (3, 4), ('a', 'b');
say "---Inside another map:";
say @array.map: {
    my @item = 1, 2, 3;
    @item.map: {
        say $_;
        }
    }
---Inside another map:
1
2
3
1
2
3
1
2
3
((True True True) (True True True) (True True True))

Hope that made sense :-)

An alternate solution would be to add a specific return value to the outer map. Then the inner map would be sunk, and therefore iterate, like so:

my @array = (1, 2), (3, 4), ('a', 'b');
say "---Inside another map:";
say @array.map: {
    my @item = 1, 2, 3;
    @item.map: {
        say $_;
        }
    42   # make sure ^^ map is sunk
    }
---Inside another map:
1
2
3
1
2
3
1
2
3
Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
  • @briandfoy: You mean letting the garbage collector emit a warning if it frees a `Seq` that was neither sunk nor iterated? – smls Jan 17 '17 at 13:46
  • As far as solutions go, the best advice is to use loop keywords like `for` (instead of functions like `map`) for imperative programming, then you won't suffer such surprises. – smls Jan 17 '17 at 13:50
  • I don't care who issues the warning. It could be a compile-time warning even. – brian d foy Jan 17 '17 at 13:54
  • @briandfoy: I don't think the compiler can be smart enough to figure this out at compile-time in the general case (e.g. method calls are resolved dynamically, so it might not even know yet that `.map` returns a `Seq`). Also, keep in mind that you wouldn't want to emit a warning just because a not-yet-iterated `Seq` is returned from a block, as that is often an intended behavior when you code in a more "functional programming" style! You'd have to catch the cases where it was probably unintentional. I'm not certain if my "garbage collector" idea would do that without causing false positives. – smls Jan 17 '17 at 14:14
  • 1
    @briandfoy: In general, Perl 6 supports more or less surprise-free "imperative programming" with `for`, `while`, array variables, assignment, etc. Other features like `map`, `X, `Z`, binding, etc. are more geared towards functional programming, and require an understanding of how laziness is handled by the language. – smls Jan 17 '17 at 14:18
  • "surprise free" hasn't been my experience. It's one of those relative things like "readability" that depends on what you like to read (or would be surprised by). – brian d foy Jan 17 '17 at 14:50