SuperCollider 4: First Thoughts

One issue here is that there is not only one kind of type conversion.

If I understand your point correctly, you’re proposing that there is a category of operations called “type conversion” and that all members of this category should behave the same – to homogenize.

But I think the category “type conversion” is itself heterogeneous. For instance, type conversion may be divided into “if-necessary” conversion (where the conversion method prefers to return the receiver if that’s appropriate) and “forced” conversion (always create and return a new object based on the receiver).

In general, method names of the format asSomething are if-necessary, while as(Something) is “forced” (since it’s a synonym for Something.newFrom(x)).

The argument seems to be that the two syntaxes asSomething and as(Something) are very similar, so they should be unified. But they mean different things. You can’t unify operations that are not the same.

A possible solution here would be to remove as(Something) and require users always to write Something.newFrom(x), and reserve asXXX for the if-necessary case – that is, to make the syntax more divergent for different operations. This also makes the problem with as(Integer) explicit – because there is no concept of a “new” Integer (or any atomic type). I guess the best you could do is:

+ Integer {
	*newFrom { |x| ^x.asInteger }
}

… but I can easily imagine someone reading this and thinking “what-the-xxxx is that for?” … and there would be no point anyway to writing Integer.newFrom(aNumber) vs aNumber.asInteger.

I think this can be explained as a case of multichannel expansion. Many places in SC accept either a single number or an array of numbers (multichannel expansion), so it isn’t completely outrageous for numeric type conversion to multichannel-expand also.

Consider this:

f = { |numberOrArrayOfNumbers|
	// now I want all of them to be floats
	if(numberOrArrayOfNumbers.isArray) {
		numberOrArrayOfNumbers = numberOrArrayOfNumbers.collect(_.asFloat);
	} {
		numberOrArrayOfNumbers = numberOrArrayOfNumbers.asFloat;
	};
	... etc...
};

I don’t think anybody really wants to be forced to do that… but if asFloat didn’t multichannel expand, this is what everyone would have to do.

Fully agreed – Array2D is not a useful class. We should deprecate it.

It’s a worthy goal to address sources of confusion. But, I’m skeptical of the idea that a confusing area in a programming language is necessarily the fault of the language. (For instance, multichannel expansion of numeric type conversion is a little confusing, but it’s also very useful! And, consistent with other idioms in SC.)

Programming language acquisition mirrors natural language acquisition. A child learning English might say “dad goed to the store” and then find out that the general rule “-ed = past tense” doesn’t apply to all verbs. Similarly, when encountering asArray and as(Array), it’s understandable to overgeneralize and collapse them into one category – but they don’t actually belong to one category. This process of overgeneralizing and then refining distinctions is a natural part of learning programming. (The opposite process – undergeneralizing and gradually coming to understand a more general principle – is another natural part of it.)

There is a temptation to say “if I have to refine my understanding, then something is badly designed in the language” but this may not always be true (or, redesigning might introduce different confusion). Lately I notice this kind of thinking in myself (“I’ve been doing this a long time, I know what I’m doing, I shouldn’t suffer confusion so it’s the software’s fault” :laughing: ) and I’m working at recognizing it and questioning whether design could really have avoided confusion.

With that said, it might actually be a good idea to get rid of as(aClass) – another case of syntax sugar sometimes backfiring.

hjh

4 Likes

Can you explain this inconsistency as noted by @fmiramar?

[1.0, 1.0].asInteger // works
[$a, $b].asInteger   // throws error
$a.asInteger         // -> 97

I’m kind of stumped, to be honest.

Edit:

$a.class.findRespondingMethodFor('asInteger') // -> nil

So why doesn’t $a.asInteger complain?

asInteger on a SequentialCollection is basically implemented as
^this.collect({ arg item; item.perform(\asInteger) });

For some reason, $a.asInteger works, but $a.perform(\asInteger) doesn’t. Don’t know why, but it’s certainly a bug.

Typical methodology for this type of question is to check the byte codes:

{ $a.asInteger }.def.dumpByteCodes

BYTECODES: (4)
  0   40       PushLiteral Character 97 'a'
  1   B0       TailCallReturnFromFunction
  2   D7       SendSpecialUnaryArithMsg 'asInteger'
  3   F2       BlockReturn

At this point, then, the short answer is that SendSpecialUnaryArithMsg handles some types internally in the C++ function, and falls back to a normal method call only if the type isn’t handled. (The longer answer is that opcode 0xD7 = 215 calls handleSendSpecialUnaryArithMsg() and this calls doSpecialUnaryArithMsg(), and here there is a long section for case tagChar:.)

When you .perform(\asInteger), it’s bypassing the special unary arithmetic message and looking for a method to implement.

But my other question about this is, why use .asInteger when a more canonical way to get the ASCII code of the character would be .ascii? (That is, it may not be good that there is an inconsistency about .asInteger, but I’m not sure we can exactly call it correct usage either. Also nobody found this for almost two decades… agreed that this is a rough edge, but it’s also a rough edge that is not sticking into anyone’s eye.)

hjh

1 Like

Thank you for the explanation!

Definitely agree, I was just curious about how one would go about figuring out the source of the behaviour - what kind of debugging strategies exist for more low-level things like this, basically. I’ve used dumpByteCodes before, but didn’t think of using it here, and I didn’t know how to connect the dots between the byte code output and where exactly in the C++ source these things are handled. Your post led me to PyrInterpreter3.cpp, so that seems at least a bit more clear to me now!

Very little of this is documented – here, I was proceeding on experience (realizing that the operation could be done by a primitive called from a class library method definition, or by the C++ implementation of the opcode, and no other way – and we know in this case that it couldn’t be the former, so it must be the latter).

I guess that’s not a very forward-looking answer but documenting all of that is a massive job and other issues are more pressing.

hjh

the .asArray vs .as(Array) is also about having too similar names.

To illustrate analogous confusions with a funny history: a friend of mine spent hours trying to find a radians converter on SuperCollider because he thought that .degrad was a bitcrusher/decimator :rofl:

I know this can be too much about personal preference, but .degrad and .raddeg wouldn’t be more clear names if swapped for .degrees and .radians ? I know that SC conversions methods are named in this style, but this .degrad name resembles MAX metaphorical naming “convention” which I find extremely confusing…

That’s quite an understatement. The in-code GUI graphs that borrow a bit from what some editors like VSCode do with its inline thingies (like for git), is quite revolutionary, IMHO. Unfortunately on Windows, Gibber puts the DWM in some mode in which I can’t take screenshots, meaning they come out all black, or I would have pasted on here…

1 Like

yep love those in-code animations - could add interest to livecoding for sure

I wonder if something like that could be hacked together for those of us using nvim …