SCLang Architecture / random questions

I am learning about programming languages in general, and have a few questions about sclang.
I’m sorry if these questions don’t make sense. Thanks if you even answer 1 of them :slight_smile:

– SCLange would be considered a dynamically-typed, imperative, strict evaluated, and interpreted language, right?

– Is sclang a “complete” language comparable to any other language, the only difference being that it’s “ecosystem” is all audio-related? i.e. if you took away all the classes, could you do the same things as in C++ with it’s standard library (or some other language)? If not, what’s the difference?

– What files would one look at to see what is “exposed” on the server, so someone could interact with it from other languages?

– I’ve seen people complain about sclang. Are their complaints mostly over syntax and personal preference, or are there legitimate problems with the language that are limiting? Ones that may eventually make it no longer able to keep up with other languages?

– Can you do lower level stuff like defining types, moving bytes around, and pointing to specific places in memory?

– Since it’s written in C/C++ and open source, does that mean one could potentially do anything to extend it? For example use JUCE for UI stuff instead of QT?

– People seem to make a big deal about languages being able to at least DO functional programming now. Doesn’t sclang have that ability? Are functions “first-class”?

– Is the “JIT” library compiling to byte code right before executing on the server?

– I see you can use QT stuff with SC. I’m curious why this was chosen over other frameworks?

– In your opinion, is SC development just as active as ever, slowing down, needing more developers, or something else?

I really love SC. I’m really passionate about it and have been for a decade now. I’m finally learning programming , although I chose to learn Haskell first and that might not be so helpful here. I’m just asking these questions so I can maybe have a better overall idea of stuff.

I don’t expect any one person or anyone to answer these really.

Thanks for reading!

-melas

There’s a lot here, but regarding the functional nature of SC…

Functions are first class on the language side, can’t pass them to the audio server (as expected).

There are no immutable variables, this kind of sucks imo.

The standard functional operations fold, reduce are all there, but they have slightly odd names. The documentation doesn’t really use them so most people don’t know how to either. I think most people adopt a imperative style of coding which is a shame as it leads to quite difficult to read code given the lack of types.
Consider this, a case a continuously reassigning a variable. In the this small example it’s okay, but quickly becomes difficult to trek track of all the changes with larger sythns, and you often need lots of odd names if you have multiple parts in there.

SynthDef(...., {
   var voice = In.ar(...);
   var voicefiltered = LPF.ar(voice, ...);
   voicefiltered = HPF.ar(voice,...);
   voicefiltered = BPF.ar(voice,...);
   ....
}).add

You could rewrite it like this…

SynthDef (...., {
   var voice = {
      var in = In.ar(); 
      [  { |v| LPF.ar(v) },
         { |v| HPF.ar(v) },
         {......}, 
      ].inject( in, { |s, f| f.(s) })
   }.();

   ...
}).add

There are two things here.
An immediately invoked function. This allows all of the voice to be defined in one step, such that the small sub steps of voice, the filtering in this case, are all hidden from the scope of the main synthdef. The rest of the code has no access to a voice that isn’t filtered.

The second thing is the inject being used on an array of functions. Inject is basically a fold in functional terms. I really like this way of working as it lets you very quickly add another operation or reorder them. It is however, a lot to type out.

Here with these small SynthDefs it looks far messier, but if you suddenly need to add a second voice it really begins to shine. Code folding also really helps as all you’d see is `voice = …’

When writing language side code you should look at flop which lets you implement your own multichannel expansion for normal functions. This is really powerful for things like loading many audio files and operations on ugens like PartConv that don’t do multichannel expansion.

I haven’t found an equivalent for the monad, but this doesn’t really occur on the server.

Also make sure you are familiar with things like collect, select, reject, (avoid do as it doesn’t return).

Anyway, that’s just my opinion on how to write functional style code in supercollider. The biggest issue is the lack of a const or let keyword, but if you just treat every var as immutable it’s okay.

2 Likes

fwiw there are const variables. These can only be used in class definitions, not functions for some reason (I suspect this may just be an oversight, since this is a very under-utilized part of sclang).

MyClass {
   const x = 1;
   *increment {
       x.postln;
       x = x + 1; // error
   }
}

Also, there are immutable objects:

~a = [1, 2, 3];
~b = [1, 2, 3].freeze;

~a[0] = 0;
~b[0] = 0; // error
1 Like

oohh, I didn’t know about freeze, thanks!

– Is sclang a “complete” language comparable to any other language, the only difference being that it’s “ecosystem” is all audio-related? i.e. if you took away all the classes, could you do the same things as in C++ with it’s standard library (or some other language)? If not, what’s the difference?

You should look up Turing completeness. Basically if a programming language can emulate a Turing machine (a really simple tape base computer) then it can compute all possible algorithms… Or something like that, I forget the exact terminology… Anyway, it turns out Turing completeness is pretty trivial to achieve, and the real problem in choosing the language which is fast enough to do what you want within the time constraint, and can express the code in a readable and maintainable manner.

So yes you can do everything you can do in C++ (or any other Turing complete language), but it might require a LOT of work and/or might take way too long to execute the code for the domain.

Jordan already provided a very deep response to some of these, but just to run through quickly…

This is about right, yes. It’s not strictly interpreted, sclang is compiled all at once to bytecode before execution, but that’s really a semantic quibble.

Yes. If there’s a difference, it’s that sclang is more heavily dependent on primitives written in C++ than many other languages like Python.

See the “Server Command Reference” doc - also, there are SC client implementations in most mainstream langauges, it’s worth searching GitHub for these before attempting anything yourself.

People like complaining about languages :slight_smile:
Honestly, there are problems with sclang, but IMO not more than other “high-level” languages like Lua, Javascript, etc. There’s perhaps a difference in that sclang was for a long time a pretty punk open source project where anyone could add anything they wanted - which means there are a lot of esoteric language details that were added without MUCH consideration of things like maintainability, unified design, etc. (see e.g. “J Concepts in SC”). These are either cool and useful, or annoying and confusing - depending on your perspective and how useful they are to you.

You can’t really point to locations in memory, but you can define types and move bytes around (you’ll be accessing them via sclang data structures and API’s, but of course this is true of any language).

Yes.

sclang functions are first class (Jordan covered this better). If you feel inclined, you can write 100% pure functional lisp-like sclang code. There’s a mix of functional and OO concepts in the core library, and the distinction is not always clear - this can make things confusing.

sclang code is compiled to byte code and then executed in an interpreter. The server doesn’t execute code at all, it only receives a description of a graph (e.g. a bunch of UGens and nodes to execute, and which order to execute them), instantiates that graph, and then produces audio with it.

I wasn’t so involved with the decision-making process here, but - when you make a list of GUI frameworks (circa 10-15 years ago…) that are:

  1. Open source and GPL-compliant.
  2. Cross-platform and embedded-friendly.
  3. Full featured, well-maintained, easy to program.
    … the list get very short, and QT is easily at the top. This has changed recently, but tbh it’s probably still the best choice.

Depends how you track it (Contributors to supercollider/supercollider · GitHub). If you factor for a few contributors who made a ton of changes in a short period, it’s basically as active as it ever was. If it’s any less active, it’s because there’s a much heavier emphasis these days on stability / maintainability, so there are maybe fewer changes happening but much higher quality. I think SC is easily in the top 10 healthiest open-source audio projects, in terms of code quality, testing, management, etc.
But I think the project badly needs people willing to take ownership of larger areas (rather than just tiny features / bug fixes). There is TONS of low-hanging fruit in terms of functionality and improvements for anyone that can write sclang or C++ and has the time to crank away at something regularly over a period of time, rather than just a weekend here or there.

Really, if anyone wants to take on a project to bump their programming knowledge or try a new challenge, I’d be happy to work with them to find something in SuperCollider that’s a good fit.

2 Likes

No. Or rather, yes you technically could, but it would be a mess as you’d have to known how the backend is altering the code you have written. The difference between String and Symbol is one such confusion (although it is actually handled quite nicely in supercollider).
Just look at the number of threads on here of people trying to output to external devices that require say, a single byte to represent a value.

Regarding types though…
If you mean a struct (a literal layout of memory), then no.

But… if you want something that behaves like a class or some type of named tuple, well there are two options, Classes, or object prototypes. I won’t mention classes as I don’t like them and there is a lot of info on them elsewhere.

Object prototypes are basically an abuse(?) or the Event, they look like this…

~mk_automotive = { | inital_pos |
	( pos: inital_pos )
};

~mk_car = { | speed_in|
	~mk_automotive.(0) ++
	(
      speed: speed_in,
      drive: {|self, seconds| self.pos = self.pos + (self.speed * seconds) }
   );
};

~brum = ~mk_car.(0.1);
~brum.drive(2);
~brum.pos == 0.2

The drive method is a bit confusing as it takes a implicit first argument of self and is called without a full-stop between ~brum and the parenthesis. The documentation is a little lacking here, which is why I’m writing this out…

Now there is another option for the inheritance. You can set the parent (or proto) of an event. Personally, I find the example above using event concatenation (++) to be much simpler to understand, but there are advantages to using parent_ and proto_, see the documentation on IdentityDictionary for more info.

~mk_automotive = {
	( pos: 0 )
};
~mk_car = { |speed_in|
	(
      speed: speed_in,
      drive: {|self, seconds| self.pos = self.pos + (self.speed * seconds) }
	).parent_(~mk_automotive.());
};

~brum = ~mk_car.(0.1);
~brum.drive(2);
~brum.pos == 0.2

There is one major draw back to this approach.

Naming conflicts.
The Event class already has a bunch of members and method defined on it.
Here trying to define the class key on the object will not work, and perhaps worse, there will be no error, as you can still access the key by using the square brackets syntax.

~mk_automotive = {
	( pos: 0 )
};
~mk_car = { |speed_in|
	~mk_automotive.()  ++ (
	  class: \car ,
      speed: speed_in,
      drive: {|self, seconds| self.pos = self.pos + (self.speed * seconds) }
	);
};

~brum = ~mk_car.(0.1);


~brum.class == ~brum[\class]   // false - oh no!

~brum.class == Event          // true
~brum[\class] == \car         // true

If you were looking for advice (if not then ignore me) I’d recommend you avoid defining methods on object prototypes, or giving them names with underscores in i.e., drive_forward as Supercollider doesn’t use snake case internally.

**
Just something extra…
It turns out that ‘global variables’ are actually members in an object (or rather an Environment which is a type of IdentityDictionary). In a sense, the only ‘global’ variable is topEnvironment. You can then change the current environment too, look at the documentation on Environment for that one. But that is moving far away from your question now…

~foo = \bar
topEnvironment.foo == \bar // true
2 Likes

Btw J concepts in SC came from James McCartney, who ranks a couple steps higher than “anybody adding anything they wanted” :wink:

hjh

Lol fair point :slight_smile: - but I think it probably still counts as esoterica, even if it’s one of my favorite parts of the language.

Is sclang a “complete” language comparable to any other language…

It’s a Smalltalk!

Smalltalk is one of the many very nice languages that arrived between 1965 and 1975 (along with Scheme, Forth, ML, Hope, Planner, Prolog, Apl, Iswim, Simula 67, Algol 68, Bcpl, Pascal &etc.).

Smalltalk turned 50 this year!

There are two open-access HOPL papers on Smalltalk, one by Alan Kay in 1993 and one by Dan Ingalls in 2020.

The surface syntax is different, there’s a different arity (or valency) rule to allow keyword and default arguments, and the system can’t be edited while it’s running, but otherwise it’s Smalltalk.

It’s a lovely language, I think you’re right to be fond of it.

Ps. There’s also the https://smalltalkzoo.thechm.org/

1 Like

@jordan

“There are no immutable variables, this kind of sucks imo.”

I was thinking about this and then we were told about “freeze” does this solve this problem?

Regarding your example of the voice variable. Now that I’m trying to learn lambda calculus / Haskell, I’ve naturally been applying what I’ve learned so far to other languages. This is a perfect example of what I’m thinking would be better than constantly re-writing variables. Sometimes I have live 10 lines of code that are just rewriting variables, but then, I also probably suck and don’t really know what I’m doing.

Regarding other functional stuff, I think are list comprehensions too, no?:

[ 2*x | x ← [1,2,3]] => [2,4,6] // that’s kinda functional right, or is that something all languages have?

Also, anonymous functions:

map ((x,y) → x+y) [(1,2),(2,3),(3,4)]
=> [3,5,7] – Sums the tuples together

Is there anything like “map” in SC? Or is this what you were saying about ‘flop’

RE: Turing completeness, I know about Turing Completeness, but I guess I was more asking can you do everything other languages can do. I think this was answered yes, but it might take forever, heh.

RE: object prototypes. Very interesting. I don’t see documentation on this.
I’m trying to wrap my head around your example, but falling short at the moment. Not seeing the main difference from Classes, but I’ve never written a class, so…

RE: Environments, I love it. Been wanting to use them more in my code, but after being absent for 6 months, I’m having to re-learn a lot of stuff. Definitely not like riding a bike, at least for me.

@scztt ‘freeze’ seems like it could be useful!

“Yes. If there’s a difference, it’s that sclang is more heavily dependent on primitives written in C++ than many other languages like Python.”

Can you type C++ code in Scide and have it work? Looks like no…Not that this is related to your answer…

"See the “Server Command Reference” " Thank you

“There’s perhaps a difference in that sclang was for a long time a pretty punk open source project where anyone could add anything they wanted - which means there are a lot of esoteric language details that were added without MUCH consideration of things like maintainability, unified design, etc. (see e.g. “J Concepts in SC”).”

And this gets to the crux of why I think SC is so badass. Ot just “has a vibe” of …cobbled together be mad geniuses.

I’m going to look into the J language thing

“sclang functions are first class (Jordan covered this better). If you feel inclined, you can write 100% pure functional lisp-like sclang code. There’s a mix of functional and OO concepts in the core library, and the distinction is not always clear - this can make things confusing.”

– Do you happen to know any people on github or any other resources of code written in a more functional style?

–The stuff about QT makes sense. Looks like it’s actually growing a lot right now. I know most long time users of SC probably don’t care or use vim or some other IDE, but I think updating the scide to look as modern as possible goes a LONG way in converting newcomers. Older looking technology that’s already foreign to people with no programming experience is daunting at best.

“Really, if anyone wants to take on a project to bump their programming knowledge or try a new challenge, I’d be happy to work with them to find something in SuperCollider that’s a good fit.”

You can probably glean from my posts that I’m pretty noobtastic, but I would LOVE to discuss any way I can help. I have been a sysadmin for 20 years, so I can figure stuff out…but, I think I’m about as passionate about SC as a person can get without being dysfunctional or creepy.

@rdd I’ve been wondering, if I could read SmallTalk material, and learn smalltalk in general, would I automatically understand sclang? They have that Pharo environment that looks pretty interesting: https://pharo.org/

[[1,2], [2,3], [3,4]].collect{|n| n[0] + n[1]}

map = collect
fold = reduce
Chaining function composition is inject
flop is like zip in python:

~a = [1,2,3,4];
~b = [\a,\b,\c,\d];
[~a, ~b].flop.collect{|n| "(n:" + n[0] + "l:" + n[1] + ")" };
// -> [ (n: 1 l: a ), (n: 2 l: b ), (n: 3 l: c ), (n: 4 l: d ) ]

Oh. It’s kind of interesting, if you look at any post anywhere about Smalltalk, you get a bunch of people saying why it sucks and/or was not successful:

(12) MountainWest RubyConf 2014 - But Really, You Should Learn Smalltalk - YouTube

i.e. Dynamic languages being crappy in large code bases, late binding and message passing making it slow, etc. I wonder if Smalltalk does “suck”, but just happens to be ok for SC’s implementation?

Maybe it’s because I’ve never worked on a really large codebase, but I never felt that dynamic typing stopped me from getting where I want to go. (It’s also worth remembering that most professional programmers are oriented towards enterprise programming, whose requirements are very different from art programming.)

I have at times suggested that SC4 could do away with sclang in favor of a general-purpose language that may be better maintained. (The SC interpreter is stable, with only one outstanding really nasty bug that I can think of.)

One requirement for a replacement language would be easy and robust operator or function/method signature overloading.

(degree: [0, 2, 4]).play;

[0, 2, 4] goes through at least degreeToKey, +, and midicps. We expect in SC that, in pretty much every case, if you can do it to a number, you can also do it to an array of numbers. UGen math also depends on overloading.

The SC implementation of overloaded operators is very clever, even beautiful – but I have to admit that it might be more straightforward in a more strongly typed language where the compiler could match method signatures based on the types of inputs.

SC is definitely slow at number crunching, for exactly this reason.

I mentioned SC’s operator overloading – it’s also worth noting that this implementation uses dynamic dispatch to avoid huge amounts of code duplication: the same performBinaryOpOnSimpleNumber handles dozens of operators. A typed language with overloading would need templates, I suppose.

I guess I’d say 1/ it was James McCartney’s prerogative to choose whatever model language he liked (because, what other individual programmer in the mid-90s would have been able to design an audio engine and a language compiler and a virtual machine to run it?); 2/ again, different requirements from enterprise programming. We aren’t retrieving a million rows from a database and aggregating.

It isn’t relevant to usage, but one of the interesting aspects of Smalltalk is that it can be implemented without any control structures in the compiler.

Control structures come down to branching and looping. Branching can be handled by having True and False classes, which implement if differently. (This actually exists in SC!) For looping – if the language has tail call optimization (which SC does have), then while can be implemented with recursion:

+ Function {
	whileRecursive { |action|
		if(this.value.not) { ^nil };  // matches 'while' exit behavior
		action.value;
		^this.whileRecursive(action)
	}
}

The trouble is that this is slower than “jump-forward” / “jump-backward” byte codes – so the SC compiler does detect some control-structure cases and generate optimized byte codes. But I find it a little bit mindbending that it’s possible to have a feature-complete programming language without any special syntax at all for flow of control. It’s useful to have one’s mind bent in this way.

(Of feature-completeness: My live-coding quark includes a parser and abstract syntax tree for a pattern-specification dialect, all written in SC.)

hjh

1 Like

The mainstream language that SC has the most in common with (probably more than even any of the Smalltalk variants) is Ruby. I think Ruby was being developed around the same time, and I recall James McCartney saying that he was explicitly borrowing things from it. Ruby has it’s own problems of course - and after doing a lot of work recently with Python 3.9/3.10 where the type checking system is incredibly rich and easy to use, I feel like Ruby may have lost a lot of it’s edge as a “dynamic” language. But nonetheless, Ruby is still a top-tier modern language, and if you were to pitch SC as “Ruby with a more fluid and flexible syntax” (… basically accurate), it would sound a whole lot more suave than “SmallTalk but even more esoteric and only for music”.

There’s a bit of a tripartite axis for programming languages, something like:

  1. Lots of type annotations
  2. Lots of unit tests
  3. Lots of unpredictable results

Languages like C++ and Rust lean heavily on (1) of course. “Classic” scripting languages like JavaScript and Python ditch (1), which leaves you with some mix of (2) and (3) - modern “typed” versions of these languages (Python with type annotations, TypeScript) are re-introducing (1) because (2) is labor-intensive and just not very fun to do, and (3) is obviously not desirable. SuperCollider chose the (2)/(3) option but never really did (2) (because: boring!) and so we’re left with a language with both feet firmly in (3), for better or for worse. Probably 60% of the messages on this forum can somehow be traced back to the choice of (3) over (1) and (2).

This is not to fault JmC or other peoples choices about SuperCollider - from a computer science perspective, (1) was highly inaccessible when SuperCollider was created. Almost NO high level languages had type checking 25+ years ago - if SC3 were a dynamic-ish, expressive high-level language with a type system when it was released, it would have been one of the ONLY things on the block doing this, a generational leap forward. SC is revolutionary in a lot of ways, this just happened to not be one of them.

In fact, the explosion of fluid and user friendly type checking for high-level dynamic languages seems like it has mainly grown out of projects like TypeScript and Python only in the last 5-7 years. If SuperCollider had a bigger community of comp sci masters and PhD students or Google engineers (meaning: people with extra-ordinary amounts of free time and domain knowledge), it would probably also have (1) at this point - but of course there aren’t so many of those people who also have a deep interest in granular synthesis :).

In conclusion: lets work on getting comp sci masters students into experimental music, so we can get type checking in SuperCollider!

1 Like

@jamshark70

“(It’s also worth remembering that most professional programmers are oriented towards enterprise programming, whose requirements are very different from art programming.)”

– THIS, this makes a lot of sense.

“I have at times suggested that SC4 could do away with sclang in favor of a general-purpose language that may be better maintained”

– Do you think this will happen soon? If so, what language would you suggest? (so I can start learning haha)

“One nasty bug that I can think of”

– which one is that?

“(Of feature-completeness: My live-coding quark includes a parser and abstract syntax tree for a pattern-specification dialect, all written in SC.)”

– Is that ddw-Chucklib-livecode ?


@scztt

“if you were to pitch SC as “Ruby with a more fluid and flexible syntax” (… basically accurate), it would sound a whole lot more suave than “SmallTalk but even more esoteric and only for music”.”

– Very interesting. I’ve never looked into Ruby, and have never heard anyone say that either. I really do wonder if the devs are considering switching to a difference language soon. Intuitively, it makes sense to choose one that is popular, flexible, and well maintained. Seems like then the devs of SC could focus on aspects of the server, but I assume that’s what they alredy do.

“In conclusion: lets work on getting comp sci masters students into experimental music, so we can get type checking in SuperCollider!”

“In fact, the explosion of fluid and user friendly type checking for high-level dynamic languages seems like it has mainly grown out of projects like TypeScript and Python only in the last 5-7 years.”

– What are the most proposed languages to replace SCLang, and do you think it will happen?

– This makes me wonder. Say we changed the language or “updated” it. What about the server? Do you think it will remain up-to-date? I guess performance wise etc.? I guess being that it’s written in C++ means it has extreme longevity in this regard?

I obviously don’t have much to contribute in the way of programming knowledge, but intuitively, the more I learn about SC and other languages, SCLang does feel kind of …confusing? Maybe a little “clunky”? But at the same time, it seems very flexible, which I think should be the case in an “art programming language”. If the syntax was more strict and language statically typed etc., it might lose the weirdness and malleability that make it fun? dunno…

There’s no concrete plan for it (neither for SC4, nor for an alternate language), so I wouldn’t worry just yet.

For what it’s worth, pros and cons of replacing sclang:

  • Pros: Another language likely has a larger developer base, meaning more careful attention to details, optimization, testing than sclang’s small, niche (and mostly nonprofessional) team can manage. JMc’s interpreter has survived over 20 years, but in that time, very very few people have looked at its internals at all – meaning, a low probability of enhancements.

  • Cons: Translating the class library would be a big job (and may run into static, if the other language is missing some features that sclang uses heavily – for instance, I think it was Lucas who was trying to translate the class library to Python, and found trouble with coroutines). Invalidating all user code in one stroke.

I would not like to see sclang to go away, but the interpreter codebase isn’t getting any younger. It may only be a matter of time.

x = [nil];
"%\n".postf(try { max(1, x) == 2 });

^^ ERROR: Message 'postf' not understood.
RECEIVER: [  ]

The receiver of postf should be the string "%\n" – but instead, the receiver is an empty array = stack corruption.

It is a bit obscure case – max(1, x) is a looping operation, because x is an array, and it throws its error from within that loop, which is caught by try. You can probably avoid the bug by pretending that try is a control structure and not relying on its result (i.e. assigning a result within the try block).

var result;
x = [nil];
try { result = (max(1, x) == 2) } { result = \failed };
"%\n".postf(result);

-> failed

Yep.

One thing that I realized recently is that there are at least three “domains” in sclang:

  • Imperative code, which works like Ruby/Smalltalk/Java/etc. E.g., if a and b are numbers, a + b performs the math operation right now, and gives you the result synchronously.

  • Synth function code, which doesn’t give you an immediate signal result. If a and b are UGens, a + b constructs another object that expresses the relationship of addition between these two.

  • Pattern code, which (like in synth functions) composes operations into new objects, but evaluates on-demand. (Something tricky here is, if there isn’t a pattern class to do exactly what you need, you might have to inject imperative code into the pattern, which requires a clear understanding of the boundaries between the pattern and imperative worlds… and it takes time to understand.)

So one expression a + b may be either imperative, DSP-composed, or pattern-composed, and it’s up to the user to be clear which world you’re in at that moment.

DSP and patterns have another difference: scsynth “pushes” signals down through the graph, while streams made from patterns “pull” from the end (and resolve a chain of dependencies to get the final result). You can see the difference with multiple references to the same random generator:

(
a = {
	var trig = Impulse.kr(1);
	var rand = TIRand.kr(0, 99, trig);
	[rand, rand].poll(trig);
	Silent.ar(1)
}.play;
)

-> Synth('temp__0' : 1000)
UGen Array [0]: 24
UGen Array [1]: 24   -- each array has two of the same number
UGen Array [0]: 42
UGen Array [1]: 42


(
var rand = Pwhite(0, 99, inf);
Ptuple([rand, rand], inf).asStream.nextN(3).do(_.postln);
)

[ 33, 61 ]  -- each array contains two different random numbers
[ 17, 90 ]
[ 50, 77 ]

The first, pushing from the top, produces one random number per trigger, and passes it down to two locations. The second, pulling from the bottom, has two independent Pwhite streams, and both are triggered independently. (By contrast, in Max/Pd, control messages and signals are both pushed from the top. This much may be easier to understand at first, but some algorithms are clumsier to express with boxes and wires.)

This sort of fine print is learnable, but someone who is starting out is likely to hit a few walls, yeah.

hjh