Async lang behaviour - how to this could be made easier for new users

If you start to learn sclang with no prior experience, there are lots of hurdles to overcome. I can vividly remember when I learned sclang in university I didn’t know anything about object oriented programming; I didn’t even know what a method call is! Learning a complex object oriented programming language like sclang will require a large amount of effort from a novice programmer.

I agree that we should strive to keep the learning curve flat, but we should also be realistic. Unpopular opinion: a classical oboist who has no idea about programming and wants to dabble in live-electronics should probably start with Max/MSP.

never mind (a)synchronous programming.

Actually, the core idea around asynchronous programming itself is rather trivial:

  1. certain operations can take an unbounded amount of time
  2. we have to wait for such operations to complete
  3. it might be nice if we could do something else in between

The problem is rather how asynchronous programming is typically done in sclang. Just a short recapitulation:

// for a single buffer, we can pass a callback function:
~buf = Buffer.read('foo.wav', action: { ... });

// For multiple buffers we need to use s.sync instead
~bufs = ~files.collect({ |x| Buffer.read(x) });
s.sync;

// Oh, but this only works if the async operation involves a *single* Server roundtrip,
// so the following does not work:
~data = [];
~bufs.do { |b| b.getToFloatArray(action: { |data| ~data = ~data.add(data) }) };
~s.sync; // nope...

// Also, s.sync only works for asynchronous operations that involve the Server, so the following doesn't work either:
~cmds.do(_.unixCmd);
~s.sync; // nope...

// So how do we actually synchronize in the last two examples? Go figure...

// Finally, getting data asynchronously with callbacks is awkward:
~buf.getn(0, 128, action: { |data| ~data = data });
s.sync;

In a promise-based model, on the other hand, all of these operations would look the same and they would be much simpler to use:

// read a single buffer and wait for completion
~buf = Buffer.read('foo.wav').await;

// Wait for multiple buffers to load
~bufs = ~files.collect({ |x| Buffer.read(x) }).await;

// Naturally, this also works for operations with several Server roundtrips:
~data = ~bufs.collect(_.getToFloatArray }).await;

// Same for async operations that don't involve the Server:
~cmds.collect(_.unixCmd).await;

// Getting data asynchronously looks the same:
~data = ~buf.getn(0, 128).await;

I hope this illustrates the point I’m trying to make. Asynchronous programming does not have to be hard!

For them to simply playback a soundfile on the server they would need to be taught all about the server/client split, then what a promise is, and that they should always remember to call await.

You don’t really need to know much about the actual client/server-architecture. The only thing you do need to know is that some operations are asynchronous and you need to await them. IMO it is not more difficult than remembering to call SynthDef.add.

how do they know whether to await or not? That is a lot to ask of a new user.

Documentation and examples.


One important thing I forgot to mention: a promise-based model also simplifies error handling because you can use exceptions, just like with ordinary synchronous method calls!

try {
~buf = Buffer.read('foo.txt').await;
} { |error|
...
}

In general, I think there is lots of truth in the adage “explicit is better than implicit” (PEP 20 – The Zen of Python | peps.python.org).

I’m not saying that your autopromise approach is bad per se, but I don’t think such “magic” belongs to basic server abstractions like Buffer. A method like numFrames should just return a value and not do some funky stuff behind the scene, such as blocking the calling thread. Waiting for completion should be done explicitly.

Actually, you can keep your internal promise object and just let the user await it with an explicit method call:

~buf = Buffer.read(`foo.wav`).await;
~buf.numFrames;

This way we can get close to a “real” promise-based model. It is not perfect because we don’t really return a promise, but it’s probably the best we can get without breaking backwards compatibility or adding lots of new dedicated methods (which would just further bloat the Class Library).

Anyway, thanks indeed for starting this discussion!

2 Likes