First time here? Check out the FAQ!
x

How can I select between arrays at run-time which are then used elsewhere (and different lengths)?

+1 vote
77 views

I have some code where I declare an array which I then access in various places. Now I want to choose between multiple arrays and I can't work out how to do that.

Here is a simplified version:

| myArray element |

myArray := #(1 2 3 4 5).
element := EventVariable new initialValue: 0.

(!Button1 nextRandomElementOf: myArray),
((!Button2 nextRandomElementOf: myArray) * 2).

I now want to select between two arrays so I want to do something like:

myArray := !ArrayChoice of: #( #(1 2 3 4 5) #(6 7 8 9 10)).

That doesn't work. But this does:

myArray := 0 of: #( #(1 2 3 4 5) #(6 7 8 9 10)).

 

This is a simplification. In my actual code I'm using this array in a dozen places and I'm using "size" and "of:" and "nextRandomElementOf:". So I can't see how I can use the nested HotVariable approach like:

!Y of: #( {!X of: #(1 2 3 4 5)} {!X of: #(6 7 8 9 10)}).

How can I choose one of these two arrays which will then be used in various places in my code later?

asked Nov 30 in Capytalk & Smalltalk by alan-jackson (Master) (6,090 points)
edited Dec 3 by alan-jackson
Could you give an example of how you're using size, of:, and nextRandomElementOf: that make it impossible for this general form to work?
!Y of: #( {!X of: #(1 2 3 4 5)} {!X of: #(6 7 8 9 10)}).
Thanks!
Here's an extract of the code...
It's the "values" array that I want to switch between two versions of.

(I've omitted a lot of code and variable declarations etc to make it more readable)



...
"The set of possible values"

values := 0 of: #(
#(0 0.5 1 {5/4} {4/3} {3/2} {9/5} 2 {18/8} {12/5} 3 {18/5} 4 6 7 {36/5} 8)
#(0 {9/8 * 0.5} {9/8} {(9/8) * (5/4)} {(9/8) *(3/2)} {(9/8) * (8/5)} {18/8})).
...


"construct the expression that generates random values"
randValsExpression := ((randVals @< 0) <+ (!RandomiseAll nextRandomElementOf: values)).

randValsExpression := (1 to: (maxRegisterSize -1)) inject: randValsExpression into: [:expr :i|
    (expr, ((randVals @< i) <+ (!RandomiseAll nextRandomElementOf: values)))].

...


"Put all the expressions together"

(randValsExpression),

...

(randVal <+ (!Gate switchedOn nextRandomElementOf: values)),

((((!Gate switchedOn nextRandom + 1) / 2) lt: !MutationRate)
            true:
                ((register @< position) <+ randVal)
            false:
                nil),

...

((!KeyDown alignWith: !Gate) true: ((register @< position) <+ ((!KeyNumber - 50) of: values)) false: nil),

(randVal <+ (!Randomise nextRandomElementOf: values)),

((!Randomise alignWith: !Gate) true: (register @< position) <+ randVal false: nil),

(register @< position).

2 Answers

0 votes

You could flatten the two Arrays into one long Array, create two masks: one with 1s where there is a value in the first Array and 0s where there was a value in the second Array (and vice versa for the second mask). Then use those masks as probability distributions for the nextRandomIndexFromDistributions:distributionIndex: Capytalk message, with !Index selecting between your two original Arrays of values. This technique can be used on an arbitrary number of Arrays.

Here is the code for convenient copy/paste:

| valueArrays flattenedArray distributions randomValue |

valueArrays := #(
    #(0 0.5 1 {5/4} {4/3} {3/2} {9/5} 2 {18/8} {12/5} 3 {18/5} 4 6 7 {36/5} 8) 
    #(0 {9/8 * 0.5} {9/8} {(9/8) * (5/4)} {(9/8) *(3/2)} {(9/8) * (8/5)} {18/8})
    ).

flattenedArray := valueArrays inject: Array new into: [ :flat :array | flat, array].

distributions :=
    1 to: valueArrays size collect: [ :i |
        (1 to: valueArrays size) inject: Array new into: [ :mask :index |
            mask, (Array new: (valueArrays at: index) size withAll: (index eq: i))]].

randomValue := (!Gate nextRandomIndexFromDistributions: distributions distributionIndex: !Index) of: flattenedArray

answered Dec 2 by ssc (Savant) (80,980 points)

Are there two "of:" messages, a CapyTalk one and a SmallTalk one?

This SmallTalk code works:

"Accessing an Array of Arrays"

| twoDArray |

twoDArray := #( #(1 2) #(3 4) #(5 6)).

2 of: (1 of: twoDArray).

..where "of:" is returning an Array, but the CapyTalk "of:" can't return an Array?

(nextRandomIndexFromDistributions looks handy, I'll use that for something else. Thanks!) The distributions approach  would work for the nextRandomElementOf: statements but not where I'm selecting an element using the keyboard.

Ultimately I want to encapsulate this class and then the values array will be a parameter entered by the user. If you think of each value array being a set of notes then it will be a common scenario to want to select between different arrays of notes (eg. different scales)  where these arrays are different lengths (eg. chromatic vs pentatonic).

To write something like this in the values parameter:

{!NoteSet of: #(
    #(1 2 3 4)
    #(5 6 7 8)
    #(9 0 1 2)
)}

would be easy but that doesn't work. I remember having this same issue when I was trying to select between different models of a ModalFilter and the arrays of frequencies I was choosing between were different lengths.

If the arrays are all the same length you can pivot the arrays and do something like this:

{!NoteSet of: #(1 5 9)}
{!NoteSet of: #(2 6 0)}
{!NoteSet of: #(3 7 1)}
{!NoteSet of: #(4 8 2)}

If they are different lengths that approach doesn't work.

In the case of the modal filter I pivotted the array of frequency arrays (as in the second !NoteSet example above), making each frequency array the same size by padding out with duplicate values using this bit of code:

    modeSet := (models collect: [ :model |
        (pos * ((model size) / largestModelSize) of: model)]).

where "model" is an array of frequencies for the modal filter, "models" is the array of "model" s, and "modeSet" is the pivotted array which allows a model to be selected using a HotValue.  The selector for the element to insert into the modeSet is "(pos * ((model size) / largestModelSize)", which is fractional for the smaller arrays and hence you get duplicate values. Duplicate values in a modal filter frequency array is not ideal and I got away with it in my application at the time (the "n:m" piece for KISS).

For this application though, duplicate values would mess up the probability distribution of selecting a value.

So how would a user of a Sound that has a parameter expecting an array choose between arrays at runtime when they are different lengths?

 

"Are there two "of:" messages, a Capytalk one and a Smalltalk one?"

Yes.

"..where "of:" is returning an Array, but the CapyTalk "of:" can't return an Array?"

Correct. The result of a Capytalk expression is a number (or an EventExpression, which is either a number, an EventValue or a combination of numbers and EventExpressions).

"For this application though, duplicate values would mess up the probability distribution of selecting a value." It sounds like you are selecting an element at random from the interior array so, in this application the flattening and masking trick should work.
"The result of a Capytalk expression is a number (or an EventExpression, which is either a number, an EventValue or a combination of numbers and EventExpressions)"

Why is that?

What I understood from what you said is this, I think:

<CapyTalk_Expression_Result> ::= <number> | <EventExpression>
<EventExpression> ::= <number> | <EventValue>
<EventExpression> ::= <EventExpression> <EventExpression>

Or something like that. That would allow a CapyTalk expression result to be 
<number> <number> <number> <number> <number>...

The KeyPitches parameter of a StepSequencer doesn't take an array, eg. "#(1 2 4)", but takes a bunch of numbers, "1 2 4".

Does that suggest that a CapyTalk expression can return a (variable length) set of numbers suitable for the StepSequencer's KeyPitches parameter, and there would be some way to change the length of that set of numbers at runtime?

(I must admit I don't feel very confident on the exact differences or definitions of "CapyTalk Expression, EventExpression, EventValue, EventVariable and HotValue".)

An EventExpression is:
i) a number
ii) an EventValue (of the form !anEV (sometimes called hot value)) OR
iii) a function of other EventExpressions (for example !anEV + 1)

When there are several EventExpressions separated by commas, the result of evaluating that expression is still a single value.

(Sorry there isn't a way to return an Array in Capytalk, but the Answer #2 is a general way to access a 2d array where the nested Arrays have different lengths).
0 votes

Here's a way to do non-random indexing of nested Arrays of arbitrary lengths using zero-based indices !Row and !Column:

Here is the code for convenient copy/paste:

| valueArrays flattenedArray rowLengths cumulativeRowLengths value |

valueArrays := #(
    #(0 0.5 1 {5/4} {4/3} {3/2} {9/5} 2 {18/8} {12/5} 3 {18/5} 4 6 7 {36/5} 8) 
    #(0 {9/8 * 0.5} {9/8} {(9/8) * (5/4)} {(9/8) *(3/2)} {(9/8) * (8/5)} {18/8})
    ).

flattenedArray := valueArrays inject: Array new into: [ :flat :array | flat, array].

rowLengths := valueArrays collect: [ :a | a size].

cumulativeRowLengths :=
    1 to: valueArrays size collect: [ :i |
        (1 to: i - 1) inject: 0 into: [ :length :index |
            length + (rowLengths at: index)]].

value := (!Row of: cumulativeRowLengths) + (!Column min: (!Row of: rowLengths) - 1) of: flattenedArray.

value

answered 6 days ago by ssc (Savant) (80,980 points)
...