Plazy's commented out asStream; downsides of enabling? (or if Plazy is "eating the resets" meant for FuncStream, here's a workaroud)

Thd Plazy code looks like this

Plazy : Pattern {
	var <>func;
	*new { arg func;
		^super.new.func_(func)
	}
	//asStream { arg ... args;
//		^func.valueArray(args).asStream
//	}
	embedInStream { arg inval;
		^func.value(inval).embedInStream(inval)
	}
	storeArgs { ^[func] }
}

The asStream seems redundant, which is perhaps why it’s commented, but it’s actually useful in one obscure case when you return a Pfunc from Plazy:

(p = Plazy{var arrs = [[1, 2], [3, 4]], apos = 0, ipos = -1; 
           Pfunc 
               { arrs @@ apos @@ (ipos = ipos + 1) }
               { apos = apos + 1 }
})

The issue of most streams “eating” the resets and not passing them down as such due to their Routine asStream implementation as been discussed before.

Basically here with Plazy as-is (commented out asStream “special”)

r = p.asStream; // -> a Routine
r.nextN(3); // -> [ 1, 2, 1 ]
r.reset; 
r.nextN(3); // -> [ 1, 2, 1 ] (i.e. same)

Whereas what the commented out “special” asStream would do:

r = p.func.value.asStream // -> a FuncStream
r.nextN(3); // -> [ 1, 2, 1 ]
r.reset; 
r.nextN(3); // -> [ 4, 3, 4 ]

So the commented out “special” asStream in Plazy could have some benefits for making Pfuncs work better inside Plazy (and Plazy’s encapsulating the state between calls for Pfuncs seems fairly useful). So are there any downsides to the “special” asStream in Plazy that’s currently commented out?

Aside: you can do something similar without Pfunc (and reasonably encapsulated) but it’s somewhat subtle how it works:

(p = Penvir((cnt: -1), Plazy {
	var parr = [Pseq([1,2], inf), Pseq([3, 4], inf)];
	parr @@ (~cnt = ~cnt + 1);
}, independent: false)); // this is essential

r = p.asStream;

r.nextN(3);
r.reset;
r.nextN(3); // -> [ 3, 4, 3 ]

p.envir[\cnt]; // -> 1

With independent: false the Penvir.envir gets initialized only in the pattern’s constructor, so it’s unaffected by stream resets. In contrast, Plazy’s entire function gets rerun on stream resets (with any and all initialization code it might contain).

You can also use a closure to capture that cnt variable in an even less accessible way

(p = value { var cnt = -1; Plazy {
	var parr = [Pseq([1,2], inf), Pseq([3, 4], inf)];
	parr @@ (cnt = cnt + 1);
}})

There’s not real reason to reinitialize the Pseq array in Plazy, so that could be moved out of Plazy too.

(p = value {
	var cnt = -1, parr = [Pseq([1,2], inf), Pseq([3, 4], inf)];
	Plazy {	parr @@ (cnt = cnt + 1) };
})

But speaking of closures… the Plazy “middle man” can replaced with a closure in the initial example from my start post too

(p = value {
	var arrs = [[1, 2], [3, 4]], apos = 0, ipos = -1;
	Pfunc { arrs @@ apos @@ (ipos = ipos + 1) } { apos = apos + 1 }
})

r = p.asStream // // -> a FuncStream (of course)

r.next
r.reset 
r.next // -> 4

In difference to storing Pseqs, this gives us replacement at cursor/“phasor” instead of after the row is finished.

My initial reaction to this is: semantically, reset suggests a return to an initial state. But here, there seems to be a desire to use reset to advance to a new initial state.

Maybe there should be a different verb?

TBH FuncStream, particularly its resetFunc, bothers me. I think its initial intent was to avoid creating a Routine for Pfunc (as Routines are relatively heavy – 27 instance variables!). But that leads to an inconsistency in the way reset is handled.

For a Routine, reset goes back to the beginning of the execution flow. If the Routine function begins with initialization code, the initialization runs before the first stream value is yielded. That’s true the first time, and also after reset.

FuncStream calls resetFunc not initially, but only upon reset. Isn’t that weird?

One consequence is that initialization behavior for Pfunc may be inconsistent with that of other patterns. For almost every pattern, you can assume that the stream will initialize itself. WIth Pfunc, the initial state has to be established externally to Pfunc.

In a way, Pfunc would make more sense like this:

PfuncTest : Pattern {
	var <>func, <>initializer;
	*new { |func, initializer|
		^super.newCopyArgs(func, initializer)
	}
	embedInStream { |inval|
		var outval;
		initializer.value(inval);
		while {
			outval = func.value(inval);
			outval.notNil
		} {
			inval = outval.yield;
		}
		^inval

		/* or maybe even:
		loop {
			outval = func.value(inval);
			inval = outval.yield;
		}
		*/
	}
}

Now, we’re kind of stuck with Pfunc as it is… but that doesn’t mean it’s right.

So are there any downsides to the “special” asStream in Plazy that’s currently commented out?

The risk of breaking somebody’s usage somewhere. I’ve seen way too many times, over the last 17-18 years, “wouldn’t it be nice if…?” leading eventually to someone else saying “I used to be able to write it like this, but it doesn’t work anymore.”

In general, if there’s an alternate way to write it that doesn’t require changes to the class library (as you found with a {}.value closure), that would be preferable to changing the class library just because the first idea seems to require a change.

hjh

Sometimes that’s what you want to get it to “play with” other stream classes, e.g. with BinaryOpXStream (see last example in the 2nd code block there).

I’ll have to apologize here: I’m afraid time doesn’t permit me to engage with a thread of that size and complexity. There are very likely some excellent ideas in it, but it’s been a grueling semester (and it’s not over yet – the Chinese academic calendar doesn’t line up with the American calendar) and I just can’t process so many examples and alternatives for now. Probably not during the summer either, as that’s the only time I’ll have to advance my music.

hjh

No problem. And no need to apologize. SC is a hobby for many, myself included. Aside: if you don’t mind me “stealing your thunder”, I’ll probably submit some PRs for the bugs you’ve basically fixed on github (cleanups de-duping, Pdef fading), but which aren’t committed yet… as those were mostly “my itch”.

Sure, no problem, go right ahead :+1:

hjh

1 Like

Alas this closure approach turned out not to be good enough solution in general because the closure-making func dens’t get re-run on asStream. Somewhat obviously, only the Pfunc gets that asStream message if you replace the Plazy with a mere closure. This results in “linked streams” if you want use that “pattern” to make more than one stream:

(p = value {
	var arrs = [[1, 2], [3, 4]], apos = 0, ipos = -1;
	Pfunc { arrs @@ apos @@ (ipos = ipos + 1) } { "inca".postln; apos = apos + 1 }
})

r = p.asStream
r.next // 1

r.reset // cool! calls inca... 
r.next // 4

// but if you "create" another stream from p
q = p.asStream
// it uses the same closure-state as other stream 
q.next // 3

And I really needed a good solution for this to solve a nastier issue with FuncStreams… (being returned from all over the place in Streams.cs) so what I came up with:

PfuncLazy : Pattern {
	var <>func;
	*new { |func| ^super.newCopyArgs(func) }
	asStream { |inval|
		var nextFunc, resetFunc;
		// don't need it for my use(s) to have inval passed to func
		// but nice to have maybe
		# nextFunc, resetFunc = func.value(inval);
		^FuncStream.new(nextFunc, resetFunc)
	}
	embedInStream { |inval|
		// only overridden because Pattern.embedInStream doesn't pass
		// an inval to asStream, otherwise would not be needed
		^this.asStream(inval).embedInStream(inval)
	}
	storeArgs { ^[func] }
}

The trick here is that both the nextFunc and resetFunc need to have access to the same closure, so they need be returned from the same closure as an array of two functions. I guess I could use an Event/Enivornment with named members for more clarity. For less clarity :grimacing: one can even write the asStream from above more briefly but somewhat cryptically just as:

	asStream { |inval| ^FuncStream(*func.value(inval)) }

Anyway, testing/using this new wonder looks like this:

(
p = PfuncLazy {
	var arrs = [[1, 2], [3, 4]], apos = 0, ipos = -1;
	[
		{ arrs @@ apos @@ (ipos = ipos + 1) }, // nextFunc
		{ "inca".postln; apos = apos + 1 }  // resetFunc
	]
}
)

r = p.asStream

r.next // 1
r.reset 
r.next // 4; ok, as with mere closure

// But now we have something different:
q = p.asStream // is really a new one
q.next // 1; not linked to the other stream
r.next // 3; ok
q.next // 2; ok

To summarize:

  • compared to a Plazy wrapper (for Pfunc), PfuncLazy will not “eat the resets itself” because it returns a FuncStream (on asStream), not a Routine.

  • unlike a mere closure (returning a Pfunc), the initialization function of a PfuncLazy gets run every time on asStream.

(I was a bit unsure as whether to call it PfuncLazy of PlazyFunc. I went for the former since primary goal is to to return a FuncStream like Pfunc, the “lazy” part is the “add-on” here. Frankly, as discussed elsewhere, Pfunc is a rather bad name given that it takes two functions. And even Plazy is not actually that lazy, since it’s returning a pattern right away, just one bound to a closure.)