Why can't I use Pkey to index an array?

Trying to use Pkey so that I can randomly choose from an array and use the same choice in 2 places in a pattern. But it seems I can’t use Pkey to index an array

(
c = ['zero', 'one', 'two', 'three'];
p = Pbind(
    \aa, Prand([0,1,2,3], inf),
    \cc, c[Pkey(\aa)]	
).asStream.nextN(5, ())
)

fails with

ERROR: Primitive '_BasicAt' failed.
Index not an Integer

whereas

(
c = ['zero', 'one', 'two', 'three'];
p = Pbind(
    \aa, Prand([0,1,2,3], inf),
    \cc, Pfunc { |e| c[e[\aa]]}	
).asStream.nextN(5, ())
)

does what I want.
Why is Pkey not returning an Integer?

(
c = ['zero', 'one', 'two', 'three'];
p = Pbind(
    \aa, Prand([0,1,2,3], inf),
	\cc, Pindex(c, Pkey(\aa))
).asStream.nextN(5, ())
)

hjh

That works if I want the entire array , but

(
c = [[0,'zero'], [1, 'one'], [2, 'two'], [3,'three']];
p = Pbind(
    \aa, Prand([0,1,2,3], inf),
\cc, Pindex(c, Pkey(\aa))[0]  
).asStream.nextN(5, ())
)

doesn’t work. And why is it necessary I’m not understanding what’s actually getting returned/embedded here.
What data type does Pkey return? It looks like an integer, so why can’t I use it as an index?

1 Like

at is a Right Now operation. It requires an array and a concrete index Right Now.

Pkey in the first example, and Pindex in the second, represent Later values.

You can’t use a Later value for a Right Now operation.

In your first example, the array is fully resolved but the index is Later = error.

In the second, the 0 index is fully realized but the array is Later = error.

I’d suggest .collect here: Pindex(c, Pkey(\aa)).collect { |row| row[0] } or Pkey(\aa).collect { |index| c[index][0] }. Now the “at” operation is being saved for later evaluation, at a time when all the information is fully resolved.

hjh

1 Like

Thank you, this feels like it’s very much in the right direction. But what do you mean by “Right Now” vs “Later”? I can’t find these phrases anywhere in documentation (or source - though I’m pretty bad at reading SC source. Is it because Pkey returns by yielding? It’s not lazy evaluation, is it?

I would really like to understand exactly what’s going on here. I’m as much baffled by why Pfunc({ |evt| c[evt[\aa]]}) works as why c[Pkey(\aa)] doesn’t.

What do I need to read understand this? And again, many thanks for the replies

These aren’t formal computer-science terms… I was trying to make it easier to understand by using simpler terminology, but that seems to have backfired.

I guess the first thing is that every SC method or function produces a result Right Now. Pkey(\aa) is really Pkey.new(\aa) and the new class method has an immediate result: an instance of Pkey.

Then, you were trying to use this instance of Pkey as an array index.

So the question is… in c[Pkey(\aa)], what is the concrete index value that this Pkey instance represents?

And the answer is… there isn’t one. At this moment, a concrete value doesn’t even exist (whether numeric or otherwise).

Pkey takes on a meaningful value only Later, when 1/ a stream is made from it and 2/ a value is requested from that stream, in that order. Pbind does these steps automatically, for you.

Maybe it would help to compare the execution steps for a couple of variants.

c = Array.fill(10, { 14.rand });

// only tracing this part; the array `c` will be assumed
(
p = Pbind(
	\aa, Pwhite(0, 9, inf),
	\cc, c[Pkey(\aa)]
).play;
)
  1. Resolve Pbind’s inputs.
    1. \aa is OK.
    2. Resolve Pwhite’s inputs.
      1. 0, OK.
      2. 9, OK.
      3. inf, OK.
    3. Execute Pwhite’s *new method with those inputs.
    4. \cc, OK.
    5. c.at(...) – resolve at’s inputs
      1. c, the receiver, is OK.
      2. Pkey – resolve the inputs.
        1. \aa, OK.
      3. Execute Pkey’s *new method with that input.
    6. Execute Array:at with c as the receiver and the Pkey *new result as the argument – here is where you get the error.
  2. Do Pbind *new with those inputs (but it never gets this far, because of error).
  3. Do play.

That is, in every method call, you can’t call a method until all of the inputs are known – which means every input expression has to be evaluated before performing the method. So we write x.abc(y.def) but abc happens last.

(
p = Pbind(
	\aa, Pwhite(0, 9, inf),
	\cc, Pfunc { |ev| c[ev[\aa]] }
).play;
)
  1. Resolve Pbind’s inputs.
    1. \aa is OK.
    2. Resolve Pwhite’s inputs.
      1. 0, OK.
      2. 9, OK.
      3. inf, OK.
    3. Execute Pwhite’s *new method with those inputs.
    4. \cc, OK.
    5. Resolve Pfunc’s input.
      1. A literal function – OK. The function contains instructions to do the c.at operation, but those instructions are not carried out at this time. Because they are not carried out at this time, there is no immediate need for a concrete index value.
    6. Perform Pfunc *new with the literal function as input.
  2. Do Pbind *new with those inputs.
  3. Do play.

Now that there is a valid Pbind instance, what happens while it’s playing?

  1. Pbind makes streams for every subpattern.
  2. Pbind loops.
    1. It receives an input event from the next caller.
    2. It loops over the name-value pairs.
      1. Pair 1: \aa, Pwhite(0, 9, inf).
        1. Get a value from the Pwhite stream.
        2. Put, into the event, \aa -> the value.
      2. Pair 2: \cc, Pfunc { |ev| c[ev[\aa]] }.
        1. Get a value from the Pfunc stream.
          1. Call the user function, with the input event as argument. So, we know now that ev must contain an \aa value which is an integer between 0 and 9.
            1. Resolve c.at input.
              1. Resolve ev.at input: \aa is OK.
              2. Perform ev.at – returns the integer index.
            2. Perform c.at with the integer as input – OK.
          2. User function returns the item from c.
        2. Put, into the event, \cc -> the value.
    3. Now the event contains \aa and \cc, so, yield it back up to the caller.

So the difference is that the version without Pfunc tries to perform at before the Pkey is resolving to a concrete index value, while the Pfunc version stores the at instruction to be performed later, when the index value exists.

hjh

First of all, thanks for the time you’re putting in to explaining this, I very much appreciate it

So Pkey(\aa) creates a Pkey instance, and adds it to the Pbind obect. When that object is played, the stream player calls that object and asks it for a value.

Pbind(...
\cc, c[Pkey(\aa)] 
)

doesn’t work, but

\cc, Pkey(\aa) + 2

does work.

This is because Pkey(\aa) + 2 is being rewritten behind the scenes though, isn’t it?
It’s really

\cc, Pbinop((whatever the symbol for + is), Pkey(\aa), 2)

Where 2 when converted to a stream, returns itself indefinitely.

What kind of resources are there for digging into the implementation? I’m having a hard time both reading the source, and finding documentation that helps me read the source.

Like seriously, what is the symbol for “+” as a binop? The doc for Pbinop Pbinop | SuperCollider 3.12.2 Help, says,
“Examples of binary operators are: +, -, /, *, min, max, hypot .”
but the example uses

Pbinop(\hypot,

not

 Pbinop(hypot,

and

Pbinop(\+

doesn’t parse

Again thanks. The key point - it’s a Pattern to stream conversion that doesn’t have an automatic implementation, is the the answer I was looking for.

https://doc.sccode.org/Reference/Adverbs.html#Adverbs%20and%20Streams

array1 +.t array2 does a “table-style” operation. I forget how the dimensions lay out, but the result is an array of arrays, and that matrix represents every combination of items from the two arrays.

array1 +.x array2 is “cross-product style” – every element from one vs every element from the other, but as a flat array.

Cross-product style makes transparent sense for two patterns (where I believe the second pattern should be finite). I’m not sure though what table style would mean for patterns, which is probably why JMc didn’t implement it.

hjh

Lifting the behavior of adverbs from collections to streams is well-defined, it’s just that nobody took the time yet. p1 op.t p2 would stream [p1_1 op p2_1, p1_1 op p2_2, ..., p1_1 op p2_n], [p1_2 op p2_1, ..., p1_2 op p2_n], … .

p2 doesn’t need to be finite to write p1 op.x p2; but you won’t iterate past the first element of p1. p2 does need to be finite for p1 op.t p2.

Often, JMc’s seemingly idiosyncratic decisions turn out, upon investigation, to have some reason behind them. For instance, there’s the recent thread about the difference between PlayBuf rate and grain units’ rate, which at first seems like an odd interface inconsistency, but which I now think is a performance optimization.

Adverbs went in all in one go (pretty much – BinaryOpXStream went in a day ahead), with t, x, s and f for arrays (integers added later), but only x for patterns. I suspect then that the omission of ‘t’ for patterns was deliberate.

The reason isn’t stated anywhere, but I’d guess probably it has to do with the very common use of infinite-length patterns and the risk of inflooping. So the approaches would be either 1/ declare it to be the user’s responsibility and just add it, or 2/ build a mandatory length limit into it. The second wouldn’t be possible with adverb syntax (there’s already a, b, operator, and adverb – where would a 5th property for b_length go?), but could be done with a new pattern class.

There’s a strong use case for it at least: a triad generator like this –

Pwhite(60, 72, inf) + Pclump(3, Pn(Pseries(0, Pwhite(-4, -3, inf), 3), inf))

– is rather awkward to write with existing patterns, but rolls off the fingers with a t adverb.

// (not implemented yet)
Pwhite(60, 72, inf) +.t Pseries(0, Pwhite(-4, -3, inf), 3)

// or, with a max-length limiting class (hypothetical):
PtableOp('+', Pwhite(60, 72, inf), Pseries(0, Pwhite(-4, -3, inf), 3), maxWidth: 3)

hjh

1 Like

Makes sense, but it just doesn’t feel right, the risk of an infinite loop is a bad sign, it depends on how you see it. Not only one can shoot the foot in this situation, but if you handle an exemption to prevent it, the right operand becomes something else. The question boils down to whether it justifies a special workflow. I think it is just not enough.

I don’t have a strong yea/nay on that. I’m skeptical – there’s no harm in seeing what it would look like, just to evaluate, not to propose formally.

Returning to the thread’s original topic: Similarly, I thought it might be nice to see what it would look like if at could compose in the way that binary operators do.

This is by no means complete – just a POC:

+ ArrayedCollection {
	at { arg index;
		_BasicAt;
		// ^this.primitiveFailed;
		// types: number, array, function, pattern, stream, ugen
		^index.performAtOnArray(this)
	}
	// clipAt, wrapAt etc.
	prAt { arg index;
		_BasicAt;
		^this.primitiveFailed;
	}
}

+ Pattern {
	// Pindex already handles streamable indices
	at { |index| ^Pindex(this, index, inf) }
}

+ Stream {
	// probably like this?
	at { |index|
		index = index.asStream;
		stream.collect { |item, inval|
			var i = index.next(inval);
			if(i.notNil) {
				item[i]
			} {
				nil
			}
		}
	}
}

// UGen 'at' makes no sense
// Pattern 'at' makes sense because a pattern could return arrays
// Multichannel UGens already return arrays
// Single-channel UGens won't be at-able anyway

// performAt
// types: number, array, function, pattern, stream, ugen
+ SimpleNumber {
	performAtOnArray { |array|
		^array.at(this)
	}
}

+ SequenceableCollection {
	// risk of infinite recursion here
	// I guess array also needs a prAt that errors out
	performAtOnArray { |array|
		^array.at(this)
	}
}

+ Pattern {
	performAtOnArray { |array|
		^Pindex(array, this, inf)
	}
}

+ Stream {
	performAtOnArray { |array|
		^Pindex(array, this, inf).asStream
	}
}

+ UGen {
	performAtOnArray { |array|
		^Select.perform(UGen.methodSelectorForRate(this.rate), this, array)
	}
}

Then:

(
p = Pbind(
	\dur, 0.25,
	\midinote, [48, 53, 55, 58, 62, 66, 73][Pwhite(0, 6, inf)]
).play;
)

p.stop;

I have to admit, I’ve got doubts about seriously pursuing this. It’s “magic,” in a way that might be obscure rather than convenient. Troubleshooting becomes more difficult in that case. Also, especially with patterns, there are several permutations that wouldn’t be possible to differentiate using [] notation (but perhaps adverbs could be applied to @ notation). For instance, if the array contains patterns, should those patterns be embedded (Pswitch), or polled for a single value (Pswitch1)? In case of Pattern:at, should the array pattern be evaluated for every next call, or only when the index pattern ends and resets? That’s 4 cases already. I probably didn’t think of everything.

It was kinda fun to do, but maybe in the end just demonstrates from a different perspective why JMc didn’t do operator composition for at.

hjh

1 Like