Making an interlace with Pif

Pif turned out to be fairly tricky to use because its condition “function” doesn’t have access to the stream Pif pulls from… so I’ve decide to post this mini-guide or exercise: make a Stream.interlace equivalent with Pif. We want something like

((:0,5..100) collate: (:2,7..100)).nextN(10)
// -> [ 0, 2, 5, 7, 10, 12, 15, 17, 20, 22 ]
// which is actually implemented as 
(:0,5..100).interlace(_<_, (:2,7..100)).nextN(10)

but done in Pbind context like (a first attempt)

(
a = (:0,5..100).p;
b = (:2,7..100).p;

Pbind(
	\x, a, \y, b,
	\z, Pif(Pkey(\x) < Pkey(\y), Pkey(\x), Pkey(\y)))
.iter.nextN(10, ()) collect: _[\z]
)
// -> [ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45 ]

That fails pretty miserably though to produce the desired result because Pbind will pull from both a and b on every iteration, so you’ll get a “zip” instead in which the values pulled from a are always less that those from b, so a wins the test every time.

Now Stream.interlace has access to the values produced previously… and actually passes one of those to the test function, but we don’t have that with Pif; we can only access the input event in the condition stream, not what the two value sub-streams (would) produce. So this is a bit like data sharing problem between two (parallel) Pbinds, except we need to share lagged data within the same Pbind.

One way is to Pclutch the input patterns and use Pfuncs for clutch conditions. And for some encapsulation read/write to a Penvir’s envir the (old) condition:

(
Penvir((oldc: false), Pbind(
	\x, Pclutch(a, Pfunc{~oldc}),
	\y, Pclutch(b, Pfunc{~oldc.not}),
	\c, Pfunc{|ev| ~oldc = ev[\x] < ev[\y]},
	\z, Pif(Pkey(\c), Pkey(\x), Pkey(\y)))
).iter.nextN(10, ()) collect: _[\z]
)

But since we’re sharing within the same lexical scope here, a closure (or a Plazy) will do just as well (A Plazy is largely equivalent to a closure, but its function gets the stream input value as argument).

(
value { var oldc = false; Pbind(
	\x, Pclutch(a, Pfunc{oldc}),
	\y, Pclutch(b, Pfunc{oldc.not}),
	\c, Pfunc{|ev| oldc = ev[\x] < ev[\y]},
	\z, Pif(Pkey(\c), Pkey(\x), Pkey(\y)))
}.iter.nextN(10, ()) collect: _[\z]
)

Now the more astute observation is that we’re not really using here Pif's ability to selectively pull from streams (since Pkeys give infinite copies of the same thing). In fact the previous code is equivalent with

(
value { var oldc = false; Pbind(
	\x, Pclutch(a, Pfunc{oldc}),
	\y, Pclutch(b, Pfunc{oldc.not}),
	\z, Pfunc{|ev| oldc = ev[\x] < ev[\y];
		if (oldc) {ev[\x]} {ev[\y]} }
)}.iter.nextN(10, ()) collect: _[\z]
)

i.e. not using Pif at all. One might wonder if we can use Pif’s stream-pulling ability instead of the Pclutch-es here. I think the answer to that is a “qualified no” because Stream.interlace pulls from both stream on its first iteration, but Pif only ever pulls from one stream. Pclutch also unconditionally pulls one value from the stream on startup; the clutch is only interrogated for subsequent pull decisions.

There is a way to “hack” an initial value pull from both stream so that Pif “gets going” in this application, but it involves some contortions with a Plazy doing an initial lookup.

(p = Plazy { |inev| // arg not really need with our a & b but otherwise...
	var ait = a.iter, bit = b.iter;
	var x = ait.next(inev), y = bit.next(inev), oldc = x < y;
	ait.reset; bit.reset;
	Pbind(
		\z, Pif(Pfunc{oldc},
			Pfunc {|ev| x = ait.next(ev)},
			Pfunc {|ev| y = bit.next(ev)}
		),
		[\x, \y], Pfunc {oldc = x < y; [x, y]}
	)
})

p.iter.nextN(10, ()) collect: _[\z]

And that honestly is pretty terrible in terms of what you have to do to (actually) “make (real) use of” Pif in this case… (We’d also need a function wrapper to pass a and b in our Plazy in a more realistic use case.) Any suggestions for alternatives in this kind of situation (well, short of actually using interlace) are welcome.

Aside:

Pbind(\z, a.iter.collate(b.iter).p).iter.nextN(10, ()) collect: _[\z]

gives an error with the stock classlib because collate (like most Stream combiners actually) returns a FuncStream and that class (unlike Routine) misses a p method. But that’s easily rectified:

+ FuncStream {
	p { ^Pfunc(nextFunc, resetFunc) }
}