FP::TransparentLazy - lazy evaluation with transparent evaluation


    use FP::TransparentLazy;

    # This is the same SYNOPSIS as in FP::Lazy but with most `force`
    # calls removed, and slightly differing behaviour in places
    # (e.g. `$a + 2` will evaluate the thunk here and thus give
    # division by zero):

    my $a = lazy { 1 / 0 };
    eval {
        # $a's evaluation is forced here
        print $a
    like $@, qr/^Illegal division by zero/;

    eval {
        $a + 2
    like $@, qr/^Illegal division by zero/;

    my $count = 0;
    my $b = lazy { $count++; 1 / 2 };
    is is_promise($b), 1;
    is $count, 0;
    is $b, 1/2; # increments $count
    is $count, 1;
    # $b is still a promise at this point (although an evaluated one):
    is is_promise($b), 1;
    is $b, 1/2; # does not increment $count anymore
    is $count, 1;

    # The following stores result of `force $b` back into $b
    FORCE $b;
    is is_promise($b), undef;
    is $b, 1/2;
    is $count, 1;

    # Note that lazy evaluation and mutation usually doesn't mix well -
    # lazy programs better be purely functional. Here $tot depends not
    # just on the inputs, but also on how many elements were evaluated:
    use FP::Stream qw(stream_map); # uses `lazy` internally
    use FP::List;
    my $tot = 0;
    my $l = stream_map sub {
        my ($x) = @_;
        $tot += $x;
    }, list (5,7,8);
    is $tot, 0;
    is $l->first, 25;
    is $tot, 5;
    is $l->length, 3;
    is $tot, 20;

    # Also note that `local` does mutation (even if in a somewhat
    # controlled way):
    our $foo = "";
    sub moo {
        my ($bar) = @_;
        local $foo = "Hello";
        lazy { "$foo $bar" }
    is moo("you")->force, " you";
    is moo("you"), " you";

    # runtime conditional lazyness:

    sub condprom {
        my ($cond) = @_;
        lazy_if { 1 / 0 } $cond

    ok is_promise(condprom 1);

    eval {
        # immediate division by zero exception (still pays
        # the overhead of two subroutine calls, though)
        condprom 0
    like $@, qr/^Illegal division by zero/;

    # A `lazyLight` promise is re-evaluated on every access:
    my $z = 0;
    my $v = lazyLight { $z++; 3*4 };
    is $v, 12;
    is $z, 1;
    is $v, 12;
    is $z, 2;
    is force($v), 12;
    is $z, 3;
    is $v, 12;
    is $z, 4;

    # There are 3 possible motivations for lazyLight: (1) lower
    # allocation cost (save the wrapper data structure); (2) no risk
    # for circular references (due to storing the result back into the
    # wrapper (mutation) that can be used recursively); (3) to get
    # fresh re-evaluation on every access and thus picking up any
    # potential side effect.

    # Arguably (3) is against the functional programming idea, and is
    # a bit of a mis-use of lazyLight. But, at least for now,
    # FP::TransparentLazy helps this case by not using `FORCE`
    # transparently; it shouldn't since that would break automatic
    # stream_ detection on subsequent calls (to things like `->map`
    # instead of `->stream_map`).

    # Note that manual use of `FORCE` still stops the re-evalution:

    ok ref $v;
    is FORCE($v), 12;
    is $z, 5;
    is $v, 12;
    is $z, 5; # you can see that re-evaluation has stopped
    ok not ref $v;

    # WARNING: such impure lazyLight promises from TransparentLazy are
    # dangerous in that if you never explicitly `force` them and use
    # the result (or `FORCE` them) then the exposure to side effects
    # will remain active. Use FP::Lazy's lazyLight instead, which
    # requires forcing thus requires this boundary to be explicit.


This implements a variant of FP::Lazy that forces promises automatically upon access (and writes their result back to the place they are forced from, like FP::Lazy's `FORCE` does, except in the lazyLight case where `FORCE` is consciously not used automatically to keep more consistent re-evaluation behaviour). Otherwise the two are fully interchangeable.

NOTE: this is EXPERIMENTAL. Also, should this be merged with Data::Thunk? OTOH, should remain interchangeable with FP::Lazy, and maybe merged with that one.

The drawback of transparency might be more confusion, as it's not directly visible anymore (neither in the debugger nor the source code) what's lazy. Also, transparent forcing will be a bit more expensive CPU wise. Please give feedback about your experiences!




This is alpha software! Read the status section in the package README or on the website.