play returns the synth instance. You cannot plot a synth, and to not return it would leak the resource.
Env gets around this by enforcing that the synth has a fixed duration.
The only reason you can plot a function is because it assumes you are not using an Out.ar but instead the return of the function should be played. Synth doesn’t have these restrictions and so getting it to work everywhere would not be possible.
You could instead return a new synth-like class that assumes the limits of the synth made by a function (FunctionSynth perhaps?) This is probably the most reasonable as its just a class containing a Synth with some extra methods.
Take a look at Function:asBuffer, Function:toFloatArray, and Function:plot, they are already pretty complex.
A case to consider – I think this captures the main difficulty. If you can find a clean way to do this, it would cover a lot of territory.
a = { (VarSaw.ar(440) * 0.1).dup }.play;
// currently not supported but wish to be
b = { SinOsc.ar(330) * 0.1).dup }.play.scope;
Two synths are played, both onto bus 0.
Presumably you would want .scope to show only b’s signal – but a and b are mixed onto bus 0.
So there’s the rub – { }.play currently assumes that the synths should be mixed onto their target bus. If the desired feature is to .plot or .scope play-ed synths separately, then… { }.play doesn’t know in advance if the signal should or shouldn’t be isolated. So you would need to do one of the following: a/ isolate every{ }.play onto temporarily allocated buses, because you might need them to be isolated, or b/ upon .scope / .plot, switch to a private bus and add a second synth to mix the signal from the private bus back into the (old) target bus.
Probably “b” is better (in that it would do the more complicated things only when needed).