Delay two Pbind

Actually, one steam can be infinite, but it should be left-hand one for the expected results.

(0..2) +.x [0, 10] // -> [ 0, 10, 1, 11, 2, 12 ]

(Pseries(0, 1) +.x Pseq([0, 10])).asStream.nextN(6)
 // -> [ 0, 10, 1, 11, 2, 12 ]

(Pseq([0, 10]) +.x Pseries(0, 1)).asStream.nextN(6) 
// -> [ 0, 1, 2, 3, 4, 5 ]

The even more confusing behavior is when you apply it to a combo of an array and an infinite stream

(Pseries(0, 1) +.x [0, 10]).asStream.nextN(3)
 // -> [ [ 0, 10 ], [ 0, 10 ], [ 0, 10 ] ]

([0, 10] +.x Pseries(0, 1)).asStream.nextN(3)
 // -> [ [ 0, 10 ], [ 1, 11 ], [ 2, 12 ] ]

You can [P]flatten the latter for the desired resultā€¦

([0, 10] +.x Pseries(0, 1)).flatten.asStream.nextN(6)
// -> [ 0, 10, 1, 11, 2, 12 ]

but thereā€™s the confusing usability issue that a finite Pseq needs to go ā€œon the rightā€, but a (finite, obvious) array needs to ā€œgo on the leftā€ . Also, in the latter case, since you have to re-flatten, itā€™s no different than using a plain ā€˜+ā€™

([0, 10] + Pseries(0, 1)).flatten.asStream.nextN(6)
// -> [ 0, 10, 1, 11, 2, 12 ]

Thereā€™s a special implementation for cross adverbs in BinaryOpXStream: when the right-hand side stream yields nil, it gets reset. (Cross adverbs are detected by Pbinop.asStream which uses BinaryOpXStream for the latter instead of BinaryOpStream.) That implementation makes it a non-commutative operation, but then so is its finite counterpart on arraysā€¦ BinaryOpXStream also works more like a flatMap in Java etc. As ā€œproofā€:

(0..2) +.x [0, 10] // same result as next line
(0..2).collect({|a| a +.x [0, 10]}).flatten // -> [ 0, 10, 1, 11, 2, 12 ]

// so, "patternizing" that idea
Pflatten(1, Pcollect({|a| a +.x [0, 10]}, Pseries(0, 1))).asStream.nextN(6) // or 
Pcollect({|a| a +.x [0, 10]}, Pseries(0, 1)).flatten.asStream.nextN(6) // or
Pseries(0, 1).collect({|a| a +.x [0, 10]}).flatten.asStream.nextN(6)
// -> [ 0, 10, 1, 11, 2, 12 ]

Because of the wrapping behavior of adding a single scalar to an array, thatā€™s the same as

(0..2).collect({|a| a + [0, 10]}).flatten //-> [ 0, 10, 1, 11, 2, 12 ]; also
Pflatten(1, Pcollect({|a| a + [0, 10]}, Pseries(0, 1))).asStream.nextN(6) // etc.

But in the more general case where Pseries returned an array, thereā€™s a difference where you flatten, or equivalently commuting the arguments of +.x (on an array), i.e row vs column ā€œmajor orderā€, e.g.

[0, 10] +.x [0, 0.1] // -> [ 0, 0.1, 10, 10.1 ]
[0, 0.1] +.x [0, 10] // -> [ 0, 10, 0.1, 10.1 ]

(Pseries([0, 0.1], 1) +.x Pseq([0, 10])).flatten.asStream.nextN(12) // same as
Pcollect({|a| [0, 10] +.x a}, Pseries([0, 0.1], 1)).flatten.asStream.nextN(12)
// -> [ 0, 0.1, 10, 10.1, 1, 1.1, 11, 11.1, 2, 2.1, 12, 12.1 ]

(Pseries([0, 0.1], 1).flatten +.x Pseq([0, 10])).asStream.nextN(12) // same as
Pcollect({|a| a +.x [0, 10]}, Pseries([0, 0.1], 1)).flatten.asStream.nextN(12)
// -> [ 0, 10, 0.1, 10.1, 1, 11, 1.1, 11.1, 2, 12, 2.1, 12.1 ]

N.B., *.x is mostly called an ā€œouterā€ rather than ā€œcrossā€ product/operation is most non-SC contexts. Actually, ā€œKronecker productā€ is probably more appropriate since the result of .x is flattened. The .t adverb in SC gives the true (unflattened) outer product.

It doesnā€™t give the expected results in this case, compare:
(I omit the chord addition as itā€™s irrelevant here)

// wanting to reduce this with adverbs:

(Pstutter(3, Pseq((60..65), inf)) + Pseq([0, -3, 3], inf)).iter.nextN(20)

-> [ 60, 57, 63, 61, 58, 64, 62, 59, 65, 63, 60, 66, 64, 61, 67, 65, 62, 68, 60, 57 ]


// both of these don't give the same

(Pseq((60..65), inf) +.x Pseq([0, -3, 3], inf)).iter.nextN(20)

-> [ 60, 57, 63, 60, 57, 63, 60, 57, 63, 60, 57, 63, 60, 57, 63, 60, 57, 63, 60, 57 ]

(Pseq((60..65)) +.x Pseq([0, -3, 3], inf)).iter.nextN(20)

-> [ 60, 57, 63, 60, 57, 63, 60, 57, 63, 60, 57, 63, 60, 57, 63, 60, 57, 63, 60, 57 ]



// That's why I suggested this

Pn(Pseq((60..65)) +.x Pseq([0, -3, 3])).iter.nextN(20)

-> [ 60, 57, 63, 61, 58, 64, 62, 59, 65, 63, 60, 66, 64, 61, 67, 65, 62, 68, 60, 57 ]

But Iā€™m sure there are alternatives :slight_smile:

Well yes, e.g. that would be one:

([0, -3, 3] +.x Pseq((60..65), inf)).flatten.iter.nextN(20)

.x is a no-op in that one; the following does the same

([0, -3, 3] + Pseq((60..65), inf)).flatten.iter.nextN(20)

The .x version needs a finite Pseq on the right.

(Pseq((60..65), inf) +.x Pseq([0, -3, 3])).iter.nextN(20)

Correct, now we have it :slight_smile:

But this thread alone shows the basic problematic of Patterns plus adverbs quite clearly:
If some elder users and an engaged newer user are making errors here and there on the way to possible solutions including adverbs, there might be an issue (and I donā€™t mean a bug).
My assumption: adverbs are a nice and short syntax, something syntactically rather pure itself ā€“ when going to apply it to the Pattern world it ends up with a lot of special cases which blurs the purity. Although I like it very much: the Pattern system is not perfect, it represents a development history and includes, as you are continuing to point out, a lot of inconsistencies, doublings etc. This is a conflict with the mathematical purity of the adverbial approach.
I can tell that I donā€™t remember a single case where I could say: ā€œbrilliant possibility to use adverbs, it makes things clearer, better readable, musically more fruitfulā€. And if a syntax is lacking such I would rarely use it and prefer to spend my time with other things.
Thatā€™s no argument of course against rethinking adverbs in general -

Thereā€™s no other adverb besides x that works for streams anyway (unlike for arrays). But even conceptually, itā€™s not terribly clear how some (other) should work for streams. ā€œRegularā€ Pbinops (that use BinaryOpStream) actually behave as if the s adverb is being used all the time. See my related/linked thread for some more musings.

Right, in my memory I was including cases with adverbs used in Pfunc, Pcollect etc. also. But same here: the exceptional usages make an advantage of syntactic sugar questionable. Iā€™m a bit arguing against my convention, some sytactic sugar I find very useful (like partial application), maybe personal preference, maybe linked to the frequency of occurence.

Over here I said: " I suppose one could implement .t for patterns by pulling all the stream values from the right operand into an array before doing the operation. That would hang the interpreter if the right-hand operand is infinite-length however ā€“ which is common for patterns, hence risky."

.t at least is conceptually not outrageous, but IMO too dangerous. Even I made the mistake of putting an infinite pattern to the right of +.x ā€“ which in that case meant only that the left pattern would draw one value only. For +.t, the same mistake would be fatal.

Also patternA + patternB.clump(n) would do something like .t, but without the riskā€¦ coming back around to Danielā€™s point (that adverbs may be less useful than they initially seem).

hjh

1 Like

If instead (or in addition to) BinaryOpXStream there was a pattern called perhaps

PforEach(outterPatten, innerPatten, itemCombineFunc)

it would probably be less confusing how to use, and not much more verbose, e.g.

PforEach(Pseq((60..65), inf), Pseq([0, -3, 3]), (_+_))

Thereā€™s actually something a bit like that with Psetp (and its subclasses, Paddp etc.) which uses the end of the value sub-stream to ā€œadvanceā€ (actually repeat) the main stream, except it only works for event streams and the combine action is limited to one field in the event as well as being ā€œhardcodedā€ in the class name (Psetp only does ā€œoverridesā€, Paddp only additions etc.)

PforEach could even work for events e.g.

PforEach(Pbind(\dur, 0.2), Pbind(\degree, Pseq([1, 2, 3]), (_.putAll(_)))

or even with more fancy actions like {|oev, iev| oev.blend(iev, 0.2)}

Actually Iā€™ve been using something like that but in a ā€œbig Proutā€ that pulls from streams based on more triggers than simply just the innerPattern hitting nil. Perhaps a more general way would be to add a separate signalling stream/pattern that decouples the value giving of the innerPattern from the mechanism that advances outterPatten, i.e. something like Pwalk-Pgate combo but with ā€œouterā€ and ā€œinnerā€-patterns instead of lists and index patterns. In fact the distinction between outer and inner becomes blurry once you add separate triggers for advancing them separately. Youā€™re just combining two (or more) patterns then. But that probably should be separate from the simpler PforEach. (Actually, the blendFrac value is pulled from another stream in my app/use-case. So technically, Iā€™m combining 3 streams: two events and one blendFrac value-stream.)

This just a sketch, but the basic idea seems to work

PforEach : Pattern {
	var <>outerPattern, <>innerPattern, <>itemCombineFunc;

	*new { arg outerPattern, innerPattern, itemCombineFunc;
		^super.newCopyArgs(outerPattern, innerPattern, itemCombineFunc)
	}

	embedInStream {  arg inval;
		var outerStr = outerPattern.asStream, innerStr = innerPattern.asStream;
		// could treat itemCombineFunc as a pattern too, but it's not clear if that helps much
		// since functions can pull from streams "on their own" anyway...
		var outerVal = outerStr.next(inval), innerVal;

		if (outerVal.isNil) { ^nil; };
		loop {
			innerVal = innerStr.next(inval);
			if (innerVal.isNil) {
				outerVal = outerStr.next(inval);
				if (outerVal.isNil) { ^nil; };
				innerStr.reset;
				innerVal = innerStr.next(inval);
				 // PforEach(1, nil, {}) would hang without next check
				if (innerVal.isNil) { ^nil; };
			};
			// Copy prevents innerVal from changing the stored outerVal.
			// Well, to some extent, it's not a deep copy.
			inval = yield(itemCombineFunc.value(outerVal.copy, innerVal));
			// Maybe outerVal, despite being "fixed" until innerVal is nil shold still be
			// updated by inval, i.e.: combin(outerVal.copy.next(inval), innerVal).
			// Perhaps this could be an option in the pattern's constructor.
		};
	}

	// TODO: handle cleanups (for event streams)
	// todo: storeOn
}

Some testing of that

PforEach(Pseq((60..65), inf), Pseq([0, -3, 3]), (_+_)).iter.nextN(20)

r = PforEach(Pbind(\dur, Pseq([0.6, 1.7])), Pbind(\degree, Pseq([1, 2])), (_.putAll(_))).iter
r.nextN(3, ());
r.nextN(3, ()); // ends on finite outer

(r = PforEach(
	Pbind(\dur, Pseries(0, 0.1)), 
	Pbind(\degree, Pseq([1, 2]), \dur, Pseq([0.5, 1])),
	(_.blend(_, 0.5))).iter)
r.nextN(3, ());
r.nextN(3, ());

// "hacks" modifiable outer sub-objects, despite (shallow) copy
(r = PforEach(
	Pbind(\farr, [11, 22]),
	Pseq([1, 2]),
	{|e| e[\farr][0] = 66} ).iter.nextN(3, ()))

Iā€™m not entirely happy with that because Pseq allows some ā€œhacksā€ to continue past nil like

Pseq([1, 2, nil, 3, 4]).iter.all // -> [ 1, 2 ]
// but...
Pseq([1, 2, nil, 3, 4]).iter.nextN(6)
// -> [ 1, 2, nil, 3, 4, nil ]

But you canā€™t use the latter with the above PforEach (nor with BinaryOpXStream for that matter) because of the reset these do. Itā€™s actually possible to write a stream that changes values on reset, but itā€™s tricky:

~cnt = -1
r = Plazy{var parr = [Pseq([1,2]), Pseq([3, 4])]; ~cnt = ~cnt + 1; parr @@ ~cnt}.iter

r.nextN(4) // -> [ 1, 2, nil, nil ]
r.reset
r.nextN(4) // -> [ 3, 4, nil, nil ]

So Iā€™m thinking that a version of PforEach that also takes as argument a custom reset-decision function could be somewhat useful too as it could allow using ā€œhackyā€ Pseqs that yield nils mid-stream.