Indeed. And that’s a useful way to delete keys from an event in a stream because Pbind itself cannot do that; if you assign a nil to a key in a Pbind, it just nils/ends the whole event stream.
And yeah, you can’t do that (delete a key) with Pkey.
Another (related) example: blend
an event with another while dropping non-common parts. Easily done with Pfunc, somewhat more tedious (but feasible) with Pnaryop
// blends via AbstractFunction, which assumes max 2 args for blend, whereas
// Dictionary.blend and subclasses take 4 args
(blend(Pbind(\foo, 2), Pbind(\foo, 1, \bar, 3), fill: false)).asStream.next(())
// -> WARNING: keyword arg 'fill' not found in call to AbstractFunction:blend
// -> ( 'bar': 3, 'foo': 1.5 )
// can't give args by name in Pnaryop; so to give 4th, need give the 3rd
Pnaryop(\blend, Pbind(\foo, 2), [Pbind(\foo, 1, \bar, 3), 0.5, false]).asStream.next(())
// -> ( 'foo': 1.5 )
// more "natural"?
(Pfunc({|ev| (foo: 2).blend(ev, fill: false)}) <> Pbind(\foo, 1, \bar, 3)).asStream.next(())
// -> ( 'foo': 1.5 )
This gets “even more natural” in a Pfunc because there’s a nasty gotcha using Dictionary.blend and merge for Events (these have a parent
that is dropped by blend/merge in the superclass implementation.)
// parent (useful: 42) is lost here;
// this is normally Event.default.parent in a .play -- ouch
e = ().parent_((useful: 42));
(Pnaryop(\blend, Pbind(\foo, 2), [Pbind(\foo, 1, \bar, 3), 0.5, false])
.asStream.next(e).parent) // -> nil
// what you really want
((Pfunc({|ev| ev.putAll((foo: 2).blend(ev, fill: false))})
<> Pbind(\foo, 1, \bar, 3)).asStream.next(e).parent)
// -> ( 'useful': 42 )
You can probably get this latter Pfunc done with nested Pnaryops (maybe in combo with Plazy if you don’t want to duplicate streams)… really terrible to read though and left as exercise to the reader.
(For the sake of this last example being entirely equivalent with the fill: false
, you’d also have to also delete keys in ev
not found in (foo: 2)
, but in practice a “directional” merge or blend as done above is more useful. And you don’t get that “directional flavor” from Dictionary.blend (or Dictionary.merge) in any (direct) parametrization of those methods, besides the “parent drop” issue. So you’d need multiple Pnaryops to implement one Pfunc.)
If you extend the (right) class with just the right method, then you can alway use just one Pnaryop. And with Events/Environments you do that most of the time (with the “object prototyping” know
trick, on by default.)
~bf = {|self, other, blendFrac=0.5| self.putAll(self.blend(other, blendFrac, false)) }
(blendFrom: ~bf, foo: 2).blendFrom((foo: 1)) // -> ( 'blendFrom': a Function, 'foo': 1.5 )
You can even hide those “methods” in the parent (that’s what Event.default.parent is mostly used for):
(foo: 3).parent_((blendFrom: ~bf)).blendFrom((foo: 2)) // -> ( 'foo': 2.5 )
So yeah, with this hack it seems you can use just one Pnaryop instead of any given Pfunc, but you have to define the function somewhere separately (which actually may be a good thing).
e = ().parent_((useful: 42, blendFrom: ~bf));
(Pnaryop(\blendFrom, Pbind(\foo, 2), [Pbind(\foo, 1, \bar, 3), 0.5, false])
.asStream.next(e).postln.parent)
// posts ( 'foo': 1.5 )
// -> ( 'blendFrom': a Function, 'useful': 42 )
Fairly contrived (in the sense that a simple \blend would do here) musical example like that, just to show how to get a custom pseudo-method called “in play”.
e = ().parent_(Event.default.parent.next((blendFrom: ~bf)))
(Pnaryop(\blendFrom, Pbind(\dur, 1, \degree, 1),
[Pbind(\dur, 0.1, \degree, 8), Pseq(0.1*(1..9)), false])
.trace.play(protoEvent: e))
Aside: I have Event.<>
(nil by default) defined via a class extension to be the same as next
, which is the same as composeEvents
, so I can write just
e = ().parent_(Event.default.parent <> (blendFrom: ~bf))
(Sometimes I forget that and give examples written like that that don’t work for other people.)
Actually, you can also use “the other parent” of an Event, namely proto
here for the same effect. Unlike parent
which gets set to Event.default.parent during play
(only) if it’s nil, proto
isn’t set by “the (Event) system”, but it is used, so you don’t have to worry about chaining that one:
e = ().proto_((blendFrom: ~bf))
Besides, I guess ZeCraum hadn’t discovered Prout
. Everything is “redundant” next to that, but of course there’s the matter of conciseness. With Prout you have to explicitly yield
and also explicitly loop
; Pfunc is more useful for simpler code that doesn’t have one-time (stream-start) initialization and “auto-yields” the last expression in an “auto-loop”. (Prout is very useful for prototyping new Pattern classes without recompiling the class lib, as it “takes the same inner code” as an embedInStream
would.)
Pfunc { |ev| something.(ev) }
Prout { |ev| init_stuff; loop { ev = yield(something.(ev)) } }
But sometimes it’s “the other way around”, Prout can be “less hairy” when you need local state, since there are no C-style “static vars” in SC (because you have co-Routine
s)
Prout{ 4.yield; 5.yield; }.asStream.nextN(3) // -> [ 4, 5, nil ]
~cnt = 0; Pfunc{ switch(~cnt = ~cnt + 1, 1, {4}, 2, {5}, {nil}) }.asStream.nextN(3)
// or
~cnt = 0; Pfunc{ if((~cnt = ~cnt + 1) < 3, {~cnt + 3}, {nil}) }.asStream.nextN(3)
You can encapsulate the “local state” in Plazy too.
Plazy { init_stuff; Pfunc { |ev| something.(ev) } }
is roughly equivalent to a Prout (with a loop), but still more tedious sometimes if you don’t want the Pfunc “auto-loop” that you need to escape from (with a nil-return).
Plazy{ var cnt = 0; Pfunc{ if((cnt = cnt + 1) < 3, {cnt + 3}, {nil}) } }.asStream.nextN(3)
(See also Penvir
and PlazyEnvir
for some other/related ways to initialization, which may even setup data sharing. There are some threads here on that.)
And no, despite the trivial examples I gave here, Prout is not redundant to Pseq.
That’s the point of most classes in the Pattern library, to make some constructs simpler to write than having to dish out a Prout (or Pfunc). But there isn’t a pre-packaged Pattern class written for every conceivable function or Routine.
I’m not sure if this was changed/fixed since 2018, but I recall it was always the case that they differed. The point of the n-class is to allow a reps
arg, which by default is 1, so unlike Pfunc it doesn’t “loop forever by default” (as a stream):
~cnt = 0; Pfuncn({(~cnt = ~cnt + 1) + 3}, 2).asStream.nextN(3)
// -> [ 4, 5, nil ]
Instead of Pfuncn’s repeat count, Pfunc also allows a custom reset function to be called when the stream resets, but I haven’t really used that (besides some experimenting) because resets don’t propagate properly. You’re better off with an initialization section in a Prout or Plazy to do the “reset logic”. (Alternatively, you can use cleanups, preferably after the double-execution bugs are fixed; but cleanups don’t work, propagation-wise, for non-event streams “by design” because they use a “reserved” field/key in the event itself for cleanup-messaging between streams… although that could be changed with a more generic Halo-type approach to cleanups, but that’s not “in the works” right now.)