Search code examples
arrayslazy-evaluationrakulazy-sequencesrakudo

About Laziness [ RAKU ]


In Raku documentation it is stated that gather-take constructs are being lazy evaluated. In the following examples I have a hard time concluding about the laziness of the constructs:

say 'Iterate to Infinity is : ', (1 ... Inf).WHAT;

say 'gather is : ', gather {
    take 0;
    my ($last, $this) = 0, 1;

    loop {
        take $this;
        ($last, $this) = $this, $last + $this;
    }
}.WHAT;

say '------------------------------------';

my @f1 = lazy gather {
    take 0;
    my ($last, $this) = 0, 1;

    loop {
        take $this;
        ($last, $this) = $this, $last + $this;
    }
}

say '@f1         : ', @f1.WHAT;
say '@f1 is lazy : ', @f1.is-lazy;

say '------------------------------------';

my @f2 = 1 ... Inf;

say '@f2         : ', @f2.WHAT;
say '@f2 is lazy : ', @f2.is-lazy;

In the first case (assignement of a Seq to @f1) if we take away the "lazy" definition then the generated Sequence (with use of gather-take) is running forever (NOT lazy).

In the second case (assignement of a Seq to @f2) @f2 becomes lazy.

Why do we have a differentiation in behaviour? although we try to do the same thing: Assign a Seq to an array in a lazy way

Can someone clarify the matter ???


Solution

  • In Raku documentation it is stated that gather-take constructs are being lazy evaluated.

    They can be. But not necessarily. They're perfectly happy to work either way.

    In a key twist, if a gather is asked if it's lazy, it returns False. This is what results in the behaviour you see.

    Take #1: One way gather behaves lazily

    gather evaluates the next element in its sequence when it's needed. This is the classic definition of lazy evaluation, as described by Wikipedia:

    lazy evaluation ... delays the evaluation of an expression until its value is needed

    Take #2: A second way gather behaves lazily

    Some constructs that consume sequences of values always consume them lazily. If the sequence they're consuming is a gather, they demand its values lazily. In which case the gather obliges, evaluating its sequence until it reaches a take, and then yielding until the next value is demanded.

    Take #3: One way gather behaves eagerly

    Some constructs that consume sequences of values always consume them eagerly. If a sequence they're consuming is a gather, they demand its values eagerly. In which case the gather obliges, and any laziness in it is moot.

    Take #4: A second way gather behaves eagerly

    Some constructs that consume sequences of values demand them either lazily or eagerly, based on the sequence's answer to an .is-lazy call on it; if it returns True, then its values are demanded lazily, otherwise they're demanded eagerly.

    And here comes a critical twist: when .is-lazy is called on a gather construct, it returns False.

    Take #5: What's happening in your examples

    say .is-lazy
    for (gather { take 42 }),                 # False
        (gather { loop { take 42 } });        # False
    

    In your @f1 = gather ... case, @f1 is being assigned a sequence that says it's not lazy. This is so even though it contains an infinite loop. @ sigil'd variables take that as a cue to eagerly assign the sequence -- and the code hangs.


    The prefix lazy creates a new Seq that lazily draws from the expression on its right. It also returns True if .is-lazy is called on it:

    say .is-lazy
    for (lazy gather { take 42 }),            # True
        (lazy gather { loop { take 42 } });   # True
    

    If an @ sigil'd variable is assigned a value that returns True for a call to .is-lazy, the assignment, and the variable, are lazy. So the code @f1 = lazy gather ... works fine.


    Finally, the sequence (1...Inf) knows it's lazy, and tells the world it is, without needing a prefix lazy:

    say .is-lazy with (1 ... Inf)             # True
    

    So assigning that also works fine, with or without lazy.


    In summary, an @ sigil'd variable gains elements lazily if a Seq assigned to it says it's lazy, and eagerly otherwise.


    You didn't ask about this, but another scenario is assigning or binding a Seq to a $ sigil'd variable, or a sigil free identifier.

    As with a @ sigil'd variable, calling .is-lazy on the $ sigil'd variable or sigil free identifier will return True or False in accord with the assigned/bound Seq.

    But then, regardless of whether that .is-lazy returns True or False, the Seq will still be iterated lazily.