Parameter object vs many parameters

I’ve just run into the following situation:

Currently, VSTPlugin.search already takes 8 arguments:
*search { arg server, dir, useDefault=true, verbose=true, wait = -1, action, save=true, parallel=true, timeout=nil;

The new release would add 2 more parameters. Now, 8 parameters already feel awkward, but 10 parameters would definitely be too much, and there might be even more in the future…

So I am considering breaking the API and changing the method signature to:

*.search { arg server, dir, options, verbose=true, wait= -1, action;

with options being an (optional) Event holding all additional settings.

Usually, I try to avoid API breaks as much as possible, but I think in this case it’s not too much of a problem because usually a project will only contain a single call to VSTPlugin.search. Also, it is easy to change
VSTPlugin.search(dir: [ "/my/vst/dir" ], save: true, parallel: false)
to
VSTPlugin.search(dir: [ "/my/vst/dir", options: ( save: true, parallel: false )).

Needless to say, I should have used this approach from the very beginning. Such much for YAGNI…


Generally, I have been thinking about the pros and cons of using the Parameter Object Pattern in a language like SuperCollider:

Pro:

  1. keeps method signature small
  2. can be easily expanded
  3. parameter order doesn’t matter
  4. names can change without breaking the API (by keeping the old names as aliases)

Con:

  1. Options not visible in auto-complete
  2. Can be more verbose compared to positional arguments

Also note that when using a dedicated class, like ServerOptions, a seperate initialization step is needed. This is not the case when using Events, which is very similar to how people emulate named parameters in languages like JS or Lua:

// JS: passing an object literal
foo({ bar: 10, baz: 5 });

-- Lua: passing a table literal
foo({ bar = 10, baz = 5 })
-- actually, because it's the only argument, we can even omit the paranthesis:
foo { bar = 10, baz = 5 }

In a language like C++, which only has positional parameters, it is clear that a function with too many parameters becomes unweildy. However, in languages with keyword arguments, this is not a real problem, because you can simply skip all the arguments that you’re not interested in (assuming they are optional).

So is it only an aesthetic issue?

IMO, Python allows to solve this in a very elegant way, because optional keyword arguments can be collected in the **kwargs parameter:

foo(a, b, **kwargs):
    print(a, b)
    for key, value in kwargs.items():
        print("{0} = {1}".format(key, value))

foo(1, b = 2, bar = 3, baz = 4) # 'bar' and 'baz' are contained in 'kwargs'

This means you don’t have to list all possible parameters in the function signature and get all the flexibility of passing an object, but all parameters look and feel the same.


What are your opinions on this matter? What is your personal limit where you would consider switching to a parameter object - if at all?

I think that, mostly because ServerOptions, a parameter object would be the most style correct solution. In many cases the use of Events seems to be quite reserved as such, except when the shortcut comes handy as with prototype based programming. I can’t find an example of using them as parameter objects, it seems that the use of clumped arrays (like in synth parameters, pbind or specs) is the norm… Even for Association and Dictionary:


"// associations can be a good way to store named data in order:
a = [\x -> 700, \y -> 200, \z -> 900];"

The example uses an array.

d = Dictionary.newFrom([\hello, 9, \whello, "world"]); // uses an array

d.getPairs; // returns an array
d.putPairs([\hello, 10, \whello, "lord", "abc", 7]); // receives an array...

Notwithstanding, it’s your choice and you can have a constructor or method that receives a Dictionary.

I think this is not quite a correct inference.

The choice of object to hold parameters depends on the context in which the parameters are being used.

Synth parameters go into OSC messages, where they are just flat lists. So it wouldn’t make sense to force users to use a more complex storage object when you could just start with a flat key-value-key-value list.

In Pbind, order is important. It would be invalid to use an event or dictionary to hold Pbind parameters because then the user would have no control over the order in which the child patterns are evaluated. So it must be an ordered collection (array).

For VSTPlugin, I believe the parameters would be random-access, meaning that physical storage order would not be important, but optimized random-access lookup would matter a lot.

If you need to search for data in an array where you have no guarantees on sorting, that will be a linear search O(n). If you’re searching through 1000 items, it would be 100 times slower than searching through 10 items.

(
f = { |array, n|
	array.detect { |item| item == n }
};
)

a = (1..10);

bench { 10000.do { f.(a, rrand(1, 10)) } };
time to run: 0.016161326999992 seconds.

a = (1..1000);

bench { 10000.do { f.(a, rrand(1, 1000)) } };
time to run: 1.12744416 seconds.

But if you do the same with a dictionary, the search in the small array is already significantly faster – and the search through the larger collection does not degrade proportionally in performance (because the hash algorithm reduces the need to scan through parts of the storage).

(
f = { |dict, n| dict[n] };
)

a = IdentityDictionary.new;
(1..10).do { |n| a.put(n, n) };

bench { 10000.do { f.(a, rrand(1, 10)) } };
time to run: 0.0021339680000665 seconds.

a = IdentityDictionary.new;
(1..1000).do { |n| a.put(n, n) };

bench { 10000.do { f.(a, rrand(1, 1000)) } };
time to run: 0.0028967959999591 seconds.

In this context, dictionaries (Events) would perform significantly better. The fact that other collection types are used in different contexts doesn’t provide any useful information about this specific context.

hjh

@lucas thanks for your reply!

I think that, mostly because ServerOptions, a parameter object would be the most style correct solution.

The thing is, ServerOptions is also the only case of a configuration object I could find in the Class Library… Also, it more or less mirrors the WorldOptions C struct, so it kind of makes sense in that context.

Another disadvantage would be that the documentation of the options would be in another file, which IMO only makes sense if there are many of them. As I’ve already mentioned, I would also have to sacrifice conciseness:

var opt = SearchOptions.new;
opt.save = false;
VSTPlugin.search(options: opt);

vs

VSTPlugin.search(options: ( save: true ));

Your point about Event vs Array is interesting, though. In my case, I would need to iterate over the object either way to catch and warn about misspelled or non-existing fields, so James’ concerns about performance - while generally true - wouldn’t matter here.

For people coming from JS, Python, Lua, etc. an Event would certainly feel more natural, but I’m not long enough in the SC game to know the expectations of SC users…

@jamshark70 I was actually hoping you would chime in :slight_smile:

Which approach would you personally choose for all those extra options?

  1. keep them as method parameters
  2. dedicated VSTSearchOptions class
  3. Event/Array object
  4. something else?

Do you know of any precedents (in or outside the SC class library) I could follow?

I think a dedicated object would help with validation.

If there’s a way to populate it from an Event, that may provide a more convenient syntax. I’m thinking about it, but it’s late here. Perhaps in the morning.

hjh

After all discredit I think we agreed. Cheers.

Thanks! So far we all seem to agree that a function with 10 or more arguments is not good :slight_smile: That’s already some valuable feedback.

I just want to point out that currently the parameter object would have only 4 members (3 Booleans and 1 Float). Of course, it might grow in the future, but it’s nowhere near the 39 (!) members of ServerOptions

Personally, I tend to prefer an Event, but if this feels strange or non-idiomatic to inexperienced SC users, I will reconsider it.

As a side note: I did in fact change the type of the plugin descriptor from an Event to a dedicated VSTPluginDesc type because it grew fast and needed extensive documentation. The transition went smoothly because member access syntax is the same after all. I could do the same with the search options, once it becomes necessary.

Personally, I tend to prefer an Event, but if this feels strange or non-idiomatic to inexperienced SC users, I will reconsider it.

You can solve the problem by allowing both an Options object and a dictionary as parameter and internally converting the dictionary to the Options object, or not, or not. I just remembered an example when you said VSTPluginDesc, SynthDef metadata parameter. [Edit: also quarks configuration files].

You can solve the problem by allowing both an Options object and a dictionary as parameter and internally converting the dictionary to the Options object, or not

Yup. I might start with a simple Event and later add a dedicated Class when I feel it makes sense, keeping the former as an option.

[Edit: also quarks configuration files].

That’s a nice one! It’s basically the SC equivalent to JSON :slight_smile:

IMO it shouldn’t be so.

(midinote: 60, dur: 2) is already a set of parameters. If this is seen as unidiomatic, then perhaps there is something missing from, or misleading in, tutorial documentation.

In any case – there are a couple of useful methods (performWithEnvir, performKeyValuePairs) for relating collections to method arguments (which, to be honest, I only suspected might exist but didn’t find them until today :grin: ) – a prototype like this is probably overkill, but it does work:

Options {
	var opt1, opt2, opt3;

	*new { |opt1 = 1, opt2 = 2, opt3 = 3|
		^opt1.asOptions(opt2, opt3)
	}

	validate {
		^this
	}
}

+ SimpleNumber {
	asOptions { |opt2, opt3|
		^Options.newCopyArgs(this, opt2, opt3).validate
	}
}

+ Environment {
	asOptions {
		^Options.performWithEnvir(\new, this).validate
	}
}

+ SequenceableCollection {
	asOptions {
		^Options.performKeyValuePairs(\new, this).validate
	}
}

With that class installed, all of the following are fine:

o = Options.new;

o = Options(3, 4, 5);

o = Options((opt2: 10));

o = Options([opt3: 200]);

o = (opt2: 10).asOptions;
// etc

hjh

This is actually a neat trick!

Indeed :grin: I knew about valueEnvir (which is used for events’ msgFunc) and I thought there was probably something similar for methods.

So one way would be to make the constructor method with all the arguments into a private method (discouraging direct use of the ordered-parameter approach) and then the public constructor would accept a collection.

OTOH it might be more readable just to .at the dictionary.

hjh