Yeah the limitation of the SC generators syntax is that you can’t assign to multiple “vars” in one expression. In normal assignments or even in Pbinds you can “vectorize” the left-hand side
#x, y = [4, 8];
x // -> 4
y // -> 8
Pbind(#[\x, \y], [12, 34]).iter.next(())
// -> ( 'y': 34, 'x': 12 )
Initially when I saw that “paired” assignment like x y <-(3..5)
is possible in comprehensions I thought it was doing that, but 2nd arg there is just the 2nd arg to do
, which is usually just a counter, unless you pass more custom object that implements an unusual do
.
One can of course assign to an array as one variable even in comprehensions, e.g.
{: c, c <- (1..4).dup(3).allTuples; c.testSomething }.all
but then it’s not really space-efficient anymore as the array gets generated, which could we problematic of large combinatorial spaces.
There’s alas no built-in tuples generator (i.e. routine) in classlib (only fully stored array with allTuples). Somewhat obscurely one can write
Pbinop(\pair, (:1..3), (:1..4), 'x').iter.nextN(5)
// -> [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 1, 4 ], [ 2, 1 ] ]
But that only works in two dimensions… at least directly like that. You can “add more dimensions” but it’s a bit tricky with binops
r = Pbinop(\pair, (:1..2), (:3..4), 'x')
Pbinop('++', r, (:5..6), 'x').iter.nextN(5)
// -> [ [ 1, 3, 5 ], [ 1, 3, 6 ], [ 1, 4, 5 ], [ 1, 4, 6 ], [ 2, 3, 5 ] ]
Although ++ is a “real” (infix) binary operator, you cannot actually shorten the latter line to
(r ++.x (:5..6)).iter.nextN(5)
// -> [ [ 1, 3 ], [ 1, 4 ], [ 2, 3 ], [ 2, 4 ], 5 ]
because that applies ++ to the whole first Stream (r
) instead of its elements.
I have a new toy…
(~allTupr = { |a, b ...ca|
var c, p = Pbinop(\pair, a, b, \x);
while { (c = ca.pop).notNil } { p = Pbinop('++', p, c, \x) };
p.iter })
~allTupr.((:1..2), (:3..4), (:5..6)).nextN(5)
// -> [ [ 1, 3, 5 ], [ 1, 3, 6 ], [ 1, 4, 5 ], [ 1, 4, 6 ], [ 2, 3, 5 ] ]
// same as
3.enum([(1..2), (3..4), (5..6)], type: 1)[..4]
But my allTupr
takes streams/routines as inputs and outputs a stream too.
The funny thing is that since duplicating stream is a no-op, one has to be careful how to write a true stream dup… by embedding it into a function an exploiting that dup(n=2)
on a function applies it n
-times.
~allTupr.(*(:1..2).dup).all
// -> [ [ 1, 2 ] ]
~allTupr.(*{(:1..2)}.dup).all
// -> [ [ 1, 1 ], [ 1, 2 ], [ 2, 1 ], [ 2, 2 ] ]
So to do…
(
b = 6.enum((1..7), { |x, i, col|
(x <= col[i-1]) and: { (i == 5).if { (col[..4].sum + x).isPrime }{ true } }
})
)
in this streamy-style
(
~isRevSorted = {|aa| var b = true;
aa.doAdjacentPairs {|x, y| b = b && (x >= y)}; b};
r = ~allTupr.(*{(:1..7)}.dup(6)).select
{ |aa| ~isRevSorted.(aa) && aa.sum.isPrime }
)
c = r.all // works but very slow
c == b // true though
The order of backing is obviously inefficient as I do it above since my tests don’t reject partially built sequences that aren’t adequately sorted. There’s probably a lot of overhead from all those stream/routine context switches too… So the (somewhat) smarter bear solution, with filter at every step
(~backTupr = { |f, a, b ...ca|
var c, p = Pbinop(\pair, a, b, \x) select: f;
while {(c = ca.pop).notNil} {p = Pbinop('++', p, c, \x) select: f};
p.iter })
r = ~backTupr.(~isRevSorted, (:1..5), (:4..6))
r.all // -> [ [ 4, 4 ], [ 5, 4 ], [ 5, 5 ] ]
(
r = ~backTupr.(~isRevSorted, *{(:1..7)}.dup(6)).select
{ |aa| aa.sum.isPrime }
)
d = r.all // somewhat faster
d == b // still ok
Still what this backTupr
has shown me is that streams have pretty substantial overhead, even when the same algorithm is “streamified”.
I’m thinking now that I could have a version of my Pforp
that allows the inner stream to skip value too somehow, i.e. not output something and just e.g. \continue
(maybe via a thrown exception). That would make an extra select
filter unnecessary at least syntax-wise… although Stream.select
is implemented as a FuncStream
, so pretty cheap anyway. At least my Pforp
turned out to have a somewhat pleasant syntax when used with (:
streams, e.g.
Pforp((:1..5), (:4..6), [_, _]).iter.select(_[0] >= _[1]).all
// also as below, but the later generas a Pselect
Pforp((:1..5), (:4..6), [_, _]).select(_[0] >= _[1]).iter.all
Actually, I can probably steal the idea from {;
and write a Pforpr
that doesn’t yield by itself, so then any “continue” is simply a matter of not yielding in the function (although this could lead to Pn-style deadlocks).