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

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

Yes, except without the underscores, so: Saw(400).LPF(300).FreeVerb(1,1)

Which would mean: FreeVerb(LPF(Saw(400),300),1,1)

It’s the same as how f(p,q) means p.f(q), except the re-writing is in the other direction.

You can already do something like this by writing ‘filter’ methods, i.e. https://scsynth.org/t/4751

Still, perhaps it would be nice if it was a general rule.

3 Likes

FYI this got merged into the main Quarks repo, so you can install and use it under the PipeFunctionChaining name now. Thank you for the nice idea, explanation, and implementation @jordan!

3 Likes

Just picking this up again. I’ve made some changes that others might be interested in…
I’ve also removed the backwards pipe, because it was just confusing and I never used it.
Happy to make a PR if liked by others - @license.

Again, these are only useful if you keep the functions short and don’t nest combinators as it gets messy quickly!
If anyone has any suggestions or useful (or even highly controversial) opinions please let me know :slight_smile:

implementation
PF {
	*new {|a| ^PF.getFunc(a) }
	*getFunc {|a|
		^a.isKindOf(Symbol).if(
			{ {|...b| a.applyTo(*b)} },
			{ a }
		)
	}
	*expand { |f|
		^{|x| PF(f).(*x) }
	}
	*id { ^{|n| n} }
	*concat {
		^{ |...x| x.reject(_.isNil) }
	}
	*concatKeepNil {
		^{ |...x| x.reject(_.isNil) }
	}
	*tap {|f|
		^{|x| PF(f).(x); x }
	}
	*par {|redux ...fs|
		if(fs.size < 1, {Error("PF.fork must have 1 or more functions").throw});

		^{|x|
			PF.concatKeepNil.(x, *fs.collect({|f| PF(f).(x) }))
			.reduce(PF(redux))
		}
	}
	*fork {|redux ...fs|
		if(fs.size < 3, {Error("PF.fork must have 3 or more functions").throw});

		^{|x|
			fs.collect({|f| PF(f).(x) })
			.reduce(PF(redux))
		}
	}
	*split {|...fs|
		^{|xs|
			if(fs.size != xs.size, {Error("Size mismatch in split").throw});
			xs.collect{|x, i| PF(fs[i]).(x) }
		}
	}
}

+Object {
	|> { |f, adverb|
		^case
		{adverb.isNil} {PF(f).(this)}
		{adverb == \fork}{
			PF.fork(*f).(this)
		}
		{adverb == \par}{
			PF.par(*f).(this)
		}
		{adverb == \tap} {
			PF.tap(f).(this)
		}

		{adverb == \split}{
			PF.split(*f).(this)
		}
		{ Error("does not understand adverb " + adverb).throw }
	}
}

+Symbol {
	|> {|f, adverb| ^PF(this).perform('|>', f, adverb) } // deffers to Function's impl
}

+Function {
	|> { |f, adverb|
		^case
		{adverb.isNil} {
			{ |...a| PF(f).(this.(*a)) }
		}
		{adverb == \fork}{
			if(f.size < 3, {Error("|>.fork must have an array of 3 or more functions").throw});
			{ |...a|
				PF.fork(*f).( this.(*a) )
			}
		}
		{adverb == \par}{
			if(f.size < 2, {Error("|>.split must have an array of 2 or more functions").throw});
			{ |...a|
				PF.par(*f).( this.(*a) )
			}
		}
		{adverb == \tap} {
			{ |...a|
				PF.tap(f).( this.(*a) )
			}
		}
		{adverb == \split}{
			{|...a|
				PF.split(*f).( this.(*a) )
			}
		}

		{ Error("does not understand adverb " + adverb).throw }

	}
}


1 Define functions

This wasn’t possible before, you had to use it there and then.

a = {|l,r| l + r } |> _.pow(2);
a.(1, 2); // produces (1+2).pow(2)

or

a = PF('+') |> _.pow(2);
a.(1, 2);

or just

a = '+' |> _.pow(2);

2 Side effects - tap

Named after its use in ruby.
Used for side effects, like outputting or writing to a buffer. It returns the input, but executes the function, ignoring its output.

SinOsc.ar(345)
|> LPF.ar(_, 565)
|>.tap Out.ar(\sideOut.kr, _) // evaluated, but output ignored
|> Out.ar(\out.kr, _)

4 Expand

Since we don’t have multiple return values this is necessary.

[1, 1] |> PF.expand('+') // 2 - behaves as reduce here

[1, 1, 1] |> PF.expand('+') // 2 '+' only takes 2 args, final 1 dropped

[1, 1, 3] |> PF.expand({|a, b, c| a + b * c}) // 6  (1+1*3)

5 Concat

++ doesn’t work with numbers and many things. This is a problem when writing generic code.

a = PF.concat;
a.(1,2,3,4)

This removes Nils as otherwise calling with things like reduce produce an extra value. This is probably an issue with reduce.
For that reason there is also PF.concatKeepNil.

This is particularly ugly…

IMHO, 1 ++ 1 should work and "asdf" ++ "fdsa" should produce ["asdf", "fdsa"].

6 Some Nary combinator / reduce thing - fork

Takes 3 or more functions.
First is a binary op, used to .reduce the others.

a 
|>.fork ['+',  _*2,  _-1,  _+1 ]

…this does …

( a * 2 ) + (a - 1) + (a + 1)

This means mid-side conversion can be written like…

stereo
|>.fork [PF.concat, PF.expand('+'), PF.expand('-')]

7 Identity

Some times .value returns the object, sometimes it doesn’t. This always returns the object.

a = PF.id
a.(30) // 30

8 Variant of fork - par

Takes 2 or more functions, first function is the reducer (as in fork), remainder are passed the object,
however… the reduce function is also passed the original object.

boost = {|sig, db| BHiShelf.ar(sig, 5000, 1, db) |> LPF.ar(_, 11000) };

WhiteNoise.ar
|>.par ['+', boost(_, 4)]

… this does…

var sig = WhiteNoise.ar;
sig = sig + boost(sig, 4);

9 Variant of expand - split

[1,2,3]
|>.split [_+1, _.pow(2), _-2]

Spread args in array over each function.

10 Adverbs also care callable like…

PF.split
This makes some things nicer to read.

|>.split [ foo, bar ]   // does the samething
|> PF.split(foo, bar)   // does the samething

example

sig
|>.fork [PF.concat, PF.expand('+'), PF.expand('-')]     // to ms
|>.split [PF.id, _ * 4.dbamp]                           // boost side
|>.tap PF.split(BufWr.ar(_, \midBuf.kr), nil)           // write mid to buffer
|>.fork [PF.concat, PF.expand('-'), PF.expand('+')]     // to stereo

I suggest you don’t make function composition and call chaining the same operator. f |> g where f is a function should evaluate as g(f), just as if f was any other type.

1 Like

I don’t understand what you mean, care to elaborate?

Right now f |> g produces a new function {|...xs| g.( f.(*xs) ) }.
What are you suggesting it produce instead?

I’m suggesting it should call g with f as an argument, in other words g(f).

Care to say why? I don’t really understand the benefit, although I get why you want to be able to pass function to other functions, but couldn’t think of an example where piping would be useful or interfere with it.

Originally, I added it as if you want to pass or store a function made by piping, would would instead have to do…

a = {|in| in |> _.pow(2) |> ... };
or
foo.reduce( {|in| in |> _.pow(2) |> ... } )

… and cannot simply write…

a = _.pow(2) |> ...
foo.reduce(_.pow(2) |> ...)

It’s a matter of design consistency and all the benefits which come from that. Piping/call chaining is one operation. Function composition is another. If you want an operator for function composition, make a new one. This also allows you to be generic over any callable (function-like object), so the programmer won’t have to remember which types are composed vs chained.

I think what @VIRTUALDOG is having a problem with is this:

*getFunc {|a|
		^a.isKindOf(Symbol).if(
			{ {|...b| a.applyTo(*b)} },
			{ a }
		)
	}

so you are using the same operator to compose functions and chain calls.

I do think that the forward function chaining operator should be part of the class lib, to recap:

defined as

=> { |that| ^that.(this) }       // I prefer => over |> !!

so that we can write {Saw.ar(300) * 0.1 => LPF.ar(_, 300)}.play

it *doesn’t` chain method calls, although you can simulate this by creating functions from each desired call:

[1, 3, 2] + [3, 4, 5] => _.collect{|x| (freq: x**2 + 800 ) } => _.do( _.play)

Now some of the methods in PF are necessary to make this fully useful

.tap is necessary in SynthDef functions (for DetectSilence!)
expand and split as well (I need these!)

For functional programming (which we are nosing up to here) we should have an Identity function!! This is important enough to have a more prominent home though. Miguel Negrao’s FPLib uses I.d. Id suggest Function.i but its about as many characters as {|i| i}.

I also agree re: ++ - I implemented this for SimpleNumber myself - I can imagine there might be a problem with that but it needs perhaps separate consideration.

Hmm, I appreciate this thanks!

I haven’t really run into an issue where an object that inherited from Function is passed in and gets composed when expecting it to be called. I suppose if the user had their own callable object this could get ambiguous as it’s behaviour would depend on its class hierarchy… but then again, that’s kinda what you’d expect for an OO language (for better or worse).

Don’t suppose you have an example in mind of where this would be ambiguous? Perhaps using classes that already exist in supercollider?

Edit:

I suppose…

{ ... } |> SynthDef(_)

… is quite confusing, do you mean to make a function that returns a synthdef, or pass the function to synthdef…