Function chaining operators - opinions? thoughts? do you like this? - a big, hopefully discursive, post

A while back I posted a question about function chaining…

… since then I’ve been using it extensively in my work and wanted to share the results. I think sometimes it actually allows for some really elegant, easy to read code. I will introduce the basics first then actually compare it to alternatives. I’d really like to know what people think, is it easier to read?

What it is

Two operators |> and <|. I shall hold off evaluating them and saying why I think they are good for a moment (if you bear with).

forward chaining ‘|>’

A way of chaining function calls together. Here, each _ becomes the result of the previous expression.

With ugens

SinOsc.ar(440) |> LPF.ar(_, \lpf.kr(15000)) |> HPF.ar(_, \hpf.kr(20)) 
|> Out.ar(\out.kr(0), _);

A sine wave that is filtered by a low pass, then high pass, then outputted.

With functions

~f = {|i|  "f".postln; i + 1; };
~g = {|i|  "g".postln; i * 2; };
~h = {|i|  "h".postln; i - 2; };

2 |> ~g |> ~f |> ~h;

// returns : g f h -> 3

backward chainging ‘<|’

This one is a little harder to wrap your head around, it is executed from the right to the left. Hold off on judgement just yet, I actually think this produces the cleanest code.

With functions

~h <| ~f <| ~g <| 2
// returns : g f h -> 3

With Ugens

Now this example is dumb, I’m not suggesting this actually be used, its just here for symmetry with forward chaining.

Out.ar(\out.kr(0), _) <| HPF.ar(_, \hpf.kr(20)) <| LPF.ar(_, \lpf.kr(15000)) <| SinOsc.ar(440)

How its implemented

Amazingly its just 6 lines of actual code and a few brackets. Thanks to both @jamshark70 and @semiquaver for helping with this.

+Object {
	|> { |f| ^f.(this) }
	<| { |f|
		^if(f.isKindOf(Function), 
			{ {|i| this.( f.(i) )} },
			{ this.(f) })
	}
}

Why I think its great!

Forwards

Consider this code, how does it read and how do you go about writing it?

{
    var foo = ...;
    var bar = ...;
	var sig = someF(foo, bar);
	sig = LPF.ar(sig, \lpf.kr());
	sig = HPF.ar(sig, \hpf.kr(1500));
	Out.ar(\out.kr(), sig);
}.play

I think the above is quite representative of standard supercollider practice. You assign variables inline and immutably where possible, but towards then end, there will be a break where you have to reassign the main signal variable just to apply some simple corrections.
This isn’t too bad…

… but the moment you have more than one signal pathway, things get more complex to unwrap.

{
	var sigA = ...;
	var sigB = ...;
	sigA = LPF.ar(sigA, \lpfA.kr());
	sigA = HPF.ar(sigA, \hpfA.kr(1500));
	sigB = LPF.ar(sigB, \lpfB.kr());
	sigB = HPF.ar(sigB, \hpfB.kr(1500));
	
	Out.ar(\out.kr(), sigA * sigB);
}

Above you must continually model each variables state as you work down the page. If mutations to sigA and sigB end up in different orders it can be very difficult to understand - leading to hard to find bugs and slower prototyping.
There are two options (as I can see)…

  1. assign a varible to each stage
{
	var sigA = ...;
	var sigB = ...;
	var sigA_lpf = LPF.ar(sigA, \lpfA.kr());
	var sigA_hpf = HPF.ar(sigA_lpf, \hpfA.kr(1500));
	var sigB_lpf = LPF.ar(sigB, \lpfB.kr());
	var sigB_hpf = HPF.ar(sigB_lpf, \hpfB.kr(1500));
	
	Out.ar(\out.kr(), sigB_hpf * sigA_hpf);
}
  1. Invoke a new scope for each variable
	var sigA_corrected = {
		var lpf = LPF.ar(sigA, \lpfA.kr());
		HPF.ar(lpf, \hpfA.kr(1500));
	}.();

Both of these sort of suck. 1 is hard to keep track of and you end up with many variable names, 2 is a little better but you can quickly end up with massive indentation.

With forward chain you get this…

{
	var sigA = ... |> HPF.ar(_, \hpfA.kr()) |> LPF.ar(_, \lpfA.kr());
	var sigB = ... |> HPF.ar(_, \hpfB.kr()) |> LPF.ar(_, \lpfB.kr());
	Out.ar(\out.kr(), sigA * sigB);
}

There is simply no need for variables here. Further, it makes it easier to see how you might abstract the problem - at least easier than in the first example above.

{
	var correct = {|in, hpf, lpf|  
		in |> HPF.ar(_, hpf) |> LPF.ar(_, lpf)    
	};
	var sigA = SinOsc.ar(20) |> correct.(_, \hpfA.kr(20), \lpfA.kr(2000));
	var sigB = SinOsc.ar(200) |> correct.(_, \hpfB.kr(20), \lpfB.kr(2000));
	Out.ar(\out.kr(), sigA * sigB);
}.play

Backwards - this one is the most useful!

Consider how you would read the following blocks of code. That is, how would you parse them into english.

  • A
SinOsc.ar(LFNoise2.kr(SinOsc.kr(50).range(200, 350)).range(200, 500), mul: 0.3);

A sine wave whose frequency is shaped like a smooth noise updating by a sine wave whose frequency is 50Hz whose range is between 200 and 350 Hz. Now that original smooth noise’s range is between 200 and 500, and the original sine wave has an amplitude of 0.3.

Ouch, that is hard to say! But this isn’t a good practice, so lets look at that…

  • B)
var freqcarrier = SinOsc.kr(50).range(200, 350);
var freq = LFNoise2.kr(freqcarrier).range(200, 500);
var sig = SinOsc.ar(freq, mul: 0.3);

There exists a frequency-carrier whose is a sine wave oscillating at 50Hz between 200, and 350.
There exists a frequency shaped like smoothed noise controlled by said frequency-carrier whose range is between 200 and 500 Hz.
There exists a signal, shaped like a sine wave oscillating at some frequency with an amplitude of 0.3.

Not too bad, but still you have to read through all the code before you get to the important line (the last line). After the first line, you are left thinking, great so there is a frequency-carrier, where does that go?, and its not until the last line that you actually see where the sound comes from and there you find the purpose of the code.

  • C)
SinOsc.ar(_, mul: 0.3) <| _.range(200, 500) <| SinOsc.ar(_) <| _.range(200, 350) <| SinOsc.ar(50)

a sine wave with 0.3 amplitude whose frequency…
… is between 200 and 500 Hz and is controled by …
… a sine wave updating …
… between 200 and 350 Hz controled by …
… a sine wave at 50 Hz

I think this is so much cleaner. From the first statement you know exactly what you are dealing with, a sine wave whose frequency changes. Then you read the important musical information next, the frequency of the sine wave. This continues where as you read from left to right, you get smaller musical details, despite the code being evaluated backwards.

I also think this is easier to write, since that you are able to start directly with the sounding object, then add detail as you move towards the right. I.e., you want to do FM synthesis so you write a sine wave, and mark the frequency with an underscore, then define its pitch, then how to changes…

Some criticisms

If you over do it, it get weird

The following is valid.

~f = {|i|  "f".postln; i + 1; };
~g = {|i|  "g".postln; i * 2; };
~h = {|i|  "h".postln; i - 2; };

~f <| ( ~g <| 1 |> ~h |> ~g ) |> ~h

Its the same as… (1 |> ~g |> ~h |> ~g |> ~f |> ~h) … but still.

Brackets

For forward chaining, you need to add brackets around each function

1 |> (_ + 1) |> (_ * 2); // works
1 |> _ + 1 |> _ * 2; // does not

It gets weirder with backwards…

_ * 2 <|  _ + 1 <| 1 // this will evaluate, but returns the wrong result.
(_ * 2) <| ( _ + 1) <| 1 // correct

However this mostly goes away if you call messages with a . on the underscore.

You cannot do multiple arguments.

I actually think this is a good thing, other languages where this is possible become quite complex, and I’m not proposing this is a replacement, but a complimentary syntax.

… actually you can - but its a mess…

-1 |> (_.neg) |> (_ * 3.5) |> { |r| 
	2 |> (_.sin) |> { |two|
		4 |> _.pow(two) |> _.round(r)
	}
}

Wrapping this up

Hopefully you’ve made it to the end of this long post!

I’d love you know what everyone thinks about this syntax. Is it useful as I’ve tired to argue, or do you disagree?

13 Likes

Awesome post.

I use the forward version in my work as well and I couldn’t live without it. I use => instead of |> however. I find it lets me type as I think without having to go backwards or wait to pick names etc.

Quick suggestion - you don’t need to encapsulate math ops (or any inline operators!)

so you can write
{ Saw.ar(400) => LPF.ar(_,900) * 0.1 => Out.ar(0,_)}
instead of
{ Saw.ar(400) => LPF.ar(_,900) => ( _*0.1 ) => Out.ar(0,_)}

or

Array.fill(8, {3.0.rrand(4)}) 
* [0.1,1]
=> _.sort 
=> {|i| i.last.dup ++ i}
=> Signal.newFrom(_)

etc etc

sc has a healthy bit of functional thinking in it - you’ve got collect/inject/reject/select …

Thanks! I mostly wrote that here as an example. I didn’t go into too much detail here, though it was long enough, a but I should probably replace those with a dot call.

What do you think of the backwards chain, particular for the FM?

Backwards chain is oh so cool. thanks!

can mix forms…

{
	SinOsc.ar(_) <| ( _.range(200,400) ) <| LFSaw.ar(_) <| Line.kr(20,200,5) 
	* 0.2 
	|> FreeVerb.ar(_,1,0.1!2)
}.play

I’m curious to see is I can thread .valueArray into this scheme or other ways to get a second argument going…

Composition operators are very nice! Writing though in defense of the A example, which I think is nice as well, when indented a little. Lots of the Sc2 examples were written this way, it can make the block structure quite clear, it works well with keyword arguments and end-of-line comments, it’s easy to pick up parts of a graph and move them about, &etc.

SinOsc.ar(
	LFNoise2.kr(
		SinOsc.kr(50).range(200, 350)
	).range(200, 500),
) * 0.3

Nope! It’s a shame but it won’t work as the syntax for underscore functions is very specific. You can’t put the .range... on the end. I don’t know if there is anything that could be done about that, but it would be somewhere in the parser and potentially quite an ordeal…

Regarding the choice of operator symbol, I chose it as ‘|’ is the pipe symbol (function piping) - it’s also used in bash for this exact purpose-, and the arrow indicates which way you go. I think this same operator gets used in other languages too and having parity with other languages is good for supercollider as a pedagogical tool.
I think ‘>>>’ is too close to ‘>’, i.e., it looks like a comparison - compare ‘=’ and ‘==’ and ‘===’. But the rest of your suggestions seem reasonable though I’m not sure why it needs to be 3 characters? Though Haskell uses >>=.

Could the code below

be transcribed as follows?

(
SinOsc.kr(50).range(200, 350)
|>
LFNoise2.kr(_).range(200, 500)
|>
SinOsc.ar(_) * 0.3
)

The idea of the forward chaining operator is excellent, but |> and <| seem not to be optically ideal. How about one pair of the followings?

  • >>>> and <<<<
  • ==> and <==
  • --> and <--
(
SinOsc.kr(50).range(200, 350)
>>>>
LFNoise2.kr(_).range(200, 500)
>>>>
SinOsc.ar(_) * 0.3
)

or

(
SinOsc.kr(50).range(200, 350)
==>
LFNoise2.kr(_).range(200, 500)
==>
SinOsc.ar(_) * 0.3
)

or

(
SinOsc.kr(50).range(200, 350)
-->
LFNoise2.kr(_).range(200, 500)
-->
SinOsc.ar(_) * 0.3
)

That’s the first readable use of mixed directions I’ve seen. I just put it in there as a joke really, so thanks for doing something useful with it!
Might get more confusing without the line break though…
I messed around with using valueWithEnvir, but it was no neater than assigning variables beforehand. What’s really needed is Haskell’s ‘do’ syntax.

On a side note, it’s interesting that |> (a monadic bind) is essentially overloading the ; operator.

OK, I understand now your symbol! I should be accustomed to it.

This is great! I rely a lot on the immediately invoked func approach. But looking forward to trying this out. Thanks!

playing with indentation:

{
  SinOsc.ar(_)
    <| ( _.range(200,400) ) 
    <| LFSaw.ar(_) 
    <| Line.kr(20,200,5) * 0.2 
  |> FreeVerb.ar(_,1,0.1!2)
}.play
1 Like

This is awesome. I would use this all the time if it was possible. It reminds me of Haskell/Tidal and how this stuff works there and I think it leads to sensible-looking code (for me) for “patch style code” like in synthdefs. I would love to see this feature!

Just a side note, but be careful with partial application/currying, there’s a few issues with it in sclang at the moment:

Thanks for bumping this old thread. I love this syntax.

1 Like

You could either suggest this as a PR for SuperCollider or in the meantime create a tiny Quark for it. I would love to use this

1 Like

I like this idea for supercollider.

Our in-house language has >>| for forward chaining application and >>= for forward chaining iteration. I find it great for readability, avoiding some of the nesting, and making function flow obvious from left to right.

Now you’ve got me thinking about backward chaining too, interesting possibilities.

Did anyone create a Quark for this yet? Or is the Plugins.quark described here a better place for it? I’d love to help make this happen - getting back into SC after a long break and I’d like to give back if I can (and see what this process is like).

I’m sure there are variations of this floating about in libraries.

There’s also this from a while back about getting something like this into the standard library: https://github.com/supercollider/supercollider/issues/2812.

Also, sort of on-topic:

In Sc p.Q isn’t allowed, so it’d be possible for it to mean Q(p), and p.Q(r) mean Q(p, r) &etc.

I.e. p.Resonz(440, 1) would mean Resonz(p, 440, 1).

If filters had rate following methods at new, this’d make writing Ugen graphs left-to-right nice also.

Sc already has at least one case-sensitive re-write rule, P(q, r) means P.new(q, r) while p(q, r) means q.p(r).

2 Likes

Nice GitHub link, didn’t think to check there!

1 Like

I made a little quark as @madskjeldgaard suggested:

It’s got the code in the OP verbatim, as well as a trimmed-down and edited version of the OP in .md and .schelp format.

I have a PR up for it in the Quarks repo, and it’s a merge away, but the conversation gave me enough pause that I wanted to see if anyone here thought there was a good reason not to merge:

The above linked conversation stopped in 2017 and it’s a different syntax. My thinking is that if there isn’t a consensus on syntax, it might make the most sense to offer different flavors of it via extensions.

1 Like

This is a little terse! but if I’m reading it right you are suggesting we might be able to write:

Saw.ar(400).LPF(_,300).FreeVerb(_,1,1)

or

Saw.ar(400)
.LPF(_,300)
.FreeVerb(_,1,1)

fab!

2 Likes