6

Infinite lazy lists are awesome!

> my @fibo = 0, 1, *+* ... *;
> say @fibo[1000];
43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875

They automatically cache their values, which is handy ... most of the time. But when working with huge Fibonacci numbers (example), this can cause memory issues.

Unfortunately, I can't figure out how to create a non-caching Fibonacci sequence. Anyone?

mscha
  • 6,509
  • 3
  • 24
  • 40
  • 2
    http://examples.perl6.org/categories/euler/prob104-moritz.html – Zaid Jan 16 '17 at 06:01
  • 2
    Thanks for the link, @Zaid. My eventual solution is [here](http://pastebin.com/xAs727Ux). It should be a lot faster than the one from Moritz, since I just keep track of the last 9 and first 14 digits of the Fibonacci numbers. – mscha Jan 16 '17 at 13:41

2 Answers2

5

One major problem is you are storing it in an array, which of course keeps all of its values.

The next problem is a little more subtle, the dotty sequence generator syntax LISTCODE ... END doesn't know how many of the previous values the CODE part is going to ask for, so it keeps all of them.
( It could look at the arity/count of the CODE, but it doesn't currently seem to from experiments at the REPL )

Then there is the problem that using &postcircumfix:<[ ]> on a Seq calls .cache on the assumption that you are going to ask for another value at some point.
( From looking at the source for Seq.AT-POS )

It's possible that a future implementation could be better at each of these drawbacks.


You could create the sequence using a different feature to get around the current limitations of the dotty sequence generator syntax.

sub fibonacci-seq (){
  gather {
    take my $a = 0;
    take my $b = 1;

    loop {
      take my $c = $a + $b;
      $a = $b;
      $b = $c;
    }
  }.lazy
}

If you are just iterating through the values you can just use it as is.

my $v;
for fibonacci-seq() {
  if $_ > 1000 {
    $v = $_;
    last;
  }
}
say $v;

my $count = 100000;
for fibonacci-seq() {
  if $count-- <= 0 {
    $v = $_;
    last;
  }
}
say chars $v; # 20899

You could also use the Iterator directly. Though this isn't necessary in most circumstances.

sub fibonacci ( UInt $n ) {
  # have to get a new iterator each time this is called
  my \iterator = fibonacci-seq().iterator;
  for ^$n {
    return Nil if iterator.pull-one =:= IterationEnd;
  }
  my \result = iterator.pull-one;
  result =:= IterationEnd ?? Nil !! result
}

If you have a recent enough version of Rakudo you can use skip-at-least-pull-one.

sub fibonacci ( UInt $n ) {
  # have to get a new iterator each time this is called
  my \result = fibonacci-seq().iterator.skip-at-least-pull-one($n);
  result =:= IterationEnd ?? Nil !! result
}

You can also implement the Iterator class directly, wrapping it in a Seq.
( this is largely how methods that return sequences are written in the Rakudo core )

sub fibonacci-seq2 () {
  Seq.new:

  class :: does Iterator {

    has Int $!a = 0;
    has Int $!b = 1;

    method pull-one {
      my $current = $!a;
      my $c = $!a + $!b;
      $!a = $!b;
      $!b = $c;
      $current;
    }

    # indicate that this should never be eagerly iterated
    # which is recommended on infinite generators
    method is-lazy ( --> True ) {}

  }.new
}
Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
Brad Gilbert
  • 33,846
  • 11
  • 78
  • 129
  • 1
    Another great answer. "the dotty sequence generator ... [caches results] ... &postcircumfix:<[ ]> on a Seq calls [caches results]". What do @Larry say are the downsides to relying on use of an `@` sigil or an *explicit* `.cache` to indicate that caching is desired? – raiph Jan 16 '17 at 06:33
  • 2
    (Am I right that, if the rules were that you only got caching if you used an `@` sigil or explicitly called `.cache`, then the answer to this SO question would be the trivial and elegant change to `my $fibo = 0, 1, *+* ... *; say $fibo[1000];`?) – raiph Jan 16 '17 at 06:38
  • "*doesn't know how many of the previous values the CODE part is going to ask for, so it keeps all of them*" -- Wait, what?? If the second part is true, that's IMO pretty bad and deserves a bug report. I also disagree with the first part, that it *can't* know how many elements to remember: It just has to look at the signature of the lambda, the way `.sort` and `.map` look at the signature of the lambda they're passed and adapt their behavior accordingly. – smls Jan 16 '17 at 08:52
  • Thanks for the extensive explanation and examples. Looks like there is no convenient way to do what I wanted. I [ended up](http://pastebin.com/xAs727Ux) just old-fashionedly iterating myself in-place, that was the easiest (but not so Perl 6 cool) way to do it. – mscha Jan 16 '17 at 13:39
  • @raiph I looked at Seq.AT-POS and it calls `.cache`. Whether it should or not is another question, and I can see arguments for both. – Brad Gilbert Jan 16 '17 at 15:36
  • @smls I was going to mention that it could be smarter, but that could have made it less clear. All I know is that using that syntax on the REPL kept increasing the memory footprint without it ever going back down. – Brad Gilbert Jan 16 '17 at 15:40
  • @BradGilbert Fwiw I asked about this and [lizmat answered on #perl6-dev](https://irclog.perlgeek.de/perl6-dev/2017-01-16#i_13926565). I appreciated lizmat answering, and appreciate the enormous amount of great work she does, but I hate the answer (which I interpret as something like "yes, having just `@` and explicit `.cache` do caching would be cool, and I tried to make it that way but couldn't get it to work"). – raiph Jan 17 '17 at 04:43
  • I'm late to the party, but this answer is referenced in the [bug report](https://rt.perl.org/Ticket/Display.html?id=126189#ticket-history) for the memory leak. RE: *doesn't know how many of the previous values the CODE part is going to ask for*; that's not true, it can look at arity to figure it out. The brief examination shows it's a memory leak in continuations so likely it's not meant to cache-everything-by-design. –  Oct 13 '17 at 13:53
5

Apparently, a noob cannot comment.

When defining a lazy iterator such as sub fibonacci-seq2, one should mark the iterator as lazy by adding a "is-lazy" method that returns True, e.g.:

method is-lazy(--> True) { }

This will allow the system to detect possible infiniloops better.

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105