Home > Raku > TIMTOWTDItime

TIMTOWTDItime

On Discord flirora wished for a way to merge list elements conditional. In this instance the condition is that any element that starts with a space is part of a group.

{
    my @a = ("apple", " banana", " peach", "blueberry", "pear", " plum", "kiwi");

    multi sub merge-spacy([]) { () }
    multi sub merge-spacy([$x is copy, *@xs]) {
        if @xs[0].?starts-with(' ') {
            $x ~= @xs.shift;
            merge-spacy([|$x, |@xs])
        } else {
            $x, |merge-spacy(@xs)
        }
    }

    dd merge-spacy(@a);
}
# OUTPUT: ("apple banana peach", "blueberry", "pear plum", "kiwi")

This functional version is neat but slow. Rakudo can’t inline recursion and doesn’t do any other optimisations yet.

my @a = ("apple", " banana", " peach", "blueberry", "pear", " plum", "kiwi");

sub merge-with(@a, &c) {
    gather while @a.shift -> $e {
        if @a && &c(@a.head) {
            @a.unshift($e ~ @a.shift)
        } else {
            take $e;
        }
    }
}

dd @a.&merge-with(*.starts-with(' '));

# OUTPUT: ("apple banana peach", "blueberry", "pear plum", "kiwi").Seq

With gather/take we don’t have to worry about recursion and the returned Seq is lazy be default. This can provide a big win if the list gets big and is not wholly consumed.

my @a = ("apple", " banana", " peach", "blueberry", "pear", " plum", "kiwi");

multi sub join(*@a, :&if!) {
    class :: does Iterable {
        method iterator {
            class :: does Iterator {
                has @.a;
                has &.if;
                method pull-one {
                    return IterationEnd unless @!a;

                    my $e = @!a.shift;
                    return $e unless @!a;

                    while &.if.(@!a.head) {
                        $e ~= @!a.shift;
                    }

                    return $e;
                }
            }.new(a => @a, if => &if)
        }
    }.new
}

.say for join(@a, if => *.starts-with(' '));

This version should please lizmat as it uses iterators. The conditional is also factored out and CORE will use the Iterator lazily wherever possible. In production code I would get rid of the return-statements and replace them with ternary operators to get a little extra performance.

The original question (that clearly got me carried away) asked for the groups to be join. Once we lost a structure it can be difficult to reconstruct it.

my @a = ("apple", " banana", " peach", "blueberry", "pear", " plum", "kiwi");

#| &c decides if the group is finished
sub group-list(@a, &c) {
   my @group;
   gather while @a {
       my $e = @a.shift;
       my $next := +@a ?? @a.head !! Nil;
       @group.push($e);
       if !c($e, $next) {
           take @group.clone;
           @group = ();
       }
   }
}

dd @a.&group-list(-> $left, $right { $right && $right.starts-with(' ')});

Here the conditional gets two elements to decide if they belong to the same group. It is also the first time I used .clone.

Thanks to a simple question I learned quite a bit. It forced me to think about the disadvantages of my first idea. Maybe code challenges should explicitly asked for more then one answer for the same question.

Categories: Raku