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

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…

That just converts symbols to their corresponding operator.

It’s the extension method of Function where composition is done.

I was a little upset when (_) didn’t work …

Re the name PF it stands for pipe function, but brevity is kinda important otherwise things get unreadable.

I think the issue is String’s conviction on being an array of chars.

Don’t quite see what you mean.

Is it that you need to do (_.foo) |> (_.bar) rather than _.foo.bar. If so, that’s a limit of partial application and the bison file is quite confusing here so I’m not too sure what the issue would be.

It’s a basic principle for software design to keep your operations consistent and uniform. Look at other languages which support these operations: in Haskell composition is ., application chaining is $. In F# composition is >>, chaining/piping is |> and |<. There’s no logical reason to merge these into the same operation.

That’s exactly what you don’t want when working in a functional paradigm, and you don’t want it in a duck typed OO language either. Functionality should be defined by interface, not by type/subclassing.

1 Like

I think this example, { ... } |> SynthDef(_), has convinced me, thanks for the suggestion!

FYI…

I think this is a terribly poor argument as I did it to preserve operator associativity, which is a kind of consistency and uniformity. Turns out, I was wrong and associativity shouldn’t be applied here.

e.g.

a + b + c + d 
is the same as
(a + b) + (c + d);

…but if the function extension is removed…

2 |> (_.foo) |> (_.bar) |> (_.car) |> (_.dar) 
is not the same as
( 2 |> (_.foo) ) |> ( (_.car) |> (_.dar) )
                     ^ because this won't work
one possible fix
( 2 => (_.foo) ) => ( (_.car) |> (_.dar) )

This means the operators are related like this (for some ops * and +)…
(((a * b) * c) * d) == ((a * b) * (c + d))
or
(((a * b) * c) * d) == ((a * (b + c)) * d)
… which is very weird

I would propose to use |> and <| as a “pipe” (application chaining, $ in haskell), F# also uses those operators. And <<> and <>> for function composition ( . in haskell).

It think it would resolve this problem, and also kind of looks similar to mainstream languages, or libraries.

(|>) :: a -> (a -> b) -> b
(<|) :: (a -> b) -> a -> b 
(<>>) :: (a -> b) -> (b -> c) -> a -> c 
(<<>) :: (b -> c) -> (a -> b) -> a -> c
2 Likes

another useful adverb perhaps is .array defined as {|that| that.valueArray(this)}

then you can write [3, 4] |>.array (_ + _)

…or { [300, 0.1] |>.array SinOsc.ar( _ , 0, _) }

1 Like

One potential issue which @julian pointed out is the current definition of <>> and <<> in NodeProxy - at first glance I don’t see this causing a problem but perhaps there is a scenario? If so another symbol could be used.

The other objection back in 2017 was that _ has problems and that we would be proliferating that issue - I don’t think this objection is reasonable as |> does not depend on _ and _ needs to be fixed in either case.

1 Like

Yes, fair enough. I’m not a heavy user of NodeProxy (which is a great lib btw).

(Also, I totally forgot I suggested exactly this feature back in 2017 (!!))

Anyway, the observation regarding application chaining and function composition is sill important. Maybe we could just start using the words apply and compose to make this difference explicit before choosing the operators.

1 Like
AbstractFunction {
    <> { arg that;
        // Function composition
        ^{|...args| this.value(that.value(*args)) }
    }
}

The function composition operator (<>) in the Standard Library maintains the distinction between function composition and invocation (compose and apply). Thus, ~x <> 5 does not execute the function.

For example:

~f = {|i| i + 1; };
~g = {|i| i * 2; };
~x = ~f <> ~g;   // Compose f after g
~y = ~g <> ~f;   // Compose g after f
~x.(2);          // Invoke the composed function x with an argument of 2
~y.(2);          // Invoke the composed function y with an argument of 2

~z = ~x <> 5;    // Results in a function, not a direct evaluation

The >>= operator is specific to Monads and serves a purpose that is not quite function composition. Function composition is somewhat a simpler case, it usually uses f . g (unless you define your own way).

>>= operator (bind) is essential for chaining operations within monadic contexts.The do-notation is essentially syntactic sugar for the original >>=.

Maybe that’s what you indeed meant above? It was not clear to me. Sorry!

Take the Maybe type constructor as an example. It is a simple Monad. It represents a computation that might fail, yielding either a Just value or Nothing:

data Person =
  Person
    { personName :: String
    , personAge  :: Int
    }

-- RETURNS A MAYBE STRING
validateName :: String -> Maybe String
validateName name 
  | length name > 0 && length name <= 50 = Just name
  | otherwise = Nothing

-- RETURNS A MAYBE INT
validateAge :: Int -> Maybe Int
validateAge age 
  | age >= 0 && age <= 130 = Just age
  | otherwise = Nothing

-- BINDS THEM IN A MONADIC CONTEXT:
validatePerson :: String -> Int -> Maybe Person
validatePerson name age =
  validateName name >>= \name' ->
    validateAge age >>= \age' -> return $ Person name' age'

-- DO-NOTATION (EXACTLY THE SAME THING)
validatePersonDo :: String -> Int -> Maybe Person
validatePersonDo name age = do
  name' <- validateName name
  age' <- validateAge age
  pure $ Person name' age'

See this: http://learnyouahaskell.com/a-fistful-of-monads

Recently on this forum, there was a discussion about the error handling strategy for String.asFloat. Currently, it returns 0 when an error occurs. Some have advocated for the program to halt entirely upon such errors. Or at least post a warning etc. Meanwhile, another proposal was to return nil. This would not be very different from the Maybe Monad pattern.

BUT: if we adopt the nil approach, how should subsequent operations process this? That’s what a context is. Without using techniques such that to handle a context, this change would be a bit harder: if String.asFloat would return nil, we’d have to adjust every part of our code to check, take and pass on the value, etc.

In SC, bind would be something like this
(This could be better with some work and thought, for simplicity it’s just one class, just a quick snippet).
(Maybe2 because there is a Maybe class somewhere already)


/*

~safeDivide = { arg x, y;
    if(y == 0) {
        Maybe2.nothing
    } {
        Maybe2.just(x / y)
    }
};

~addOne = { arg x;
    Maybe2.just(x + 1)
};

Maybe2.just(5) >>=  ~safeDivide.(_,0)  >>= ~addOne;
// -> Nothing

Maybe2.just(5) >>= ~safeDivide.(_, 2) >>= ~addOne;
// -> Just 3.5

*/

Maybe2 {
    var <>value;

    *new { arg value=nil;
        ^super.newCopyArgs(value)
    }

    *nothing {
        ^this.new(nil)
    }

    *just { arg value;
        ^this.new(value)
    }

    isNothing {
        ^value.isNil
    }

    bind { arg func;
        if(this.isNothing) {
        ^this  // return current instance if it's Nothing
        } {
        ^func.(value)
        }
    }

    >>= { arg func;

        ^this.bind(func)
    }

    asString {
        ^if(this.isNothing) {
            "Nothing"
        } {
            "Just " ++ value
        }
    }
}

or:

/*

~add_one = { arg x; x + 1 };
~double = { arg x; x * 2 };

Maybe3(3) >>= ~add_one >>= ~double;
// -> Just 8

Maybe3(nil).bind(~add_one).bind(~double);
Maybe3(nil) >>= ~add_one >>=~double;
// -> Nothing

Maybe3(nil).bind(~add_one).bind(~double).orElse(10);
// -> Just 10

Maybe3(nil) | Maybe3(1);
// -> Just 1
*/

Maybe3 {
	var <value;

	*new { arg value=nil;
		^super.newCopyArgs(value)
	}

	bind { arg func;
		if(value.isNil) {
			^Maybe3(nil)
		} {
			^Maybe3(func.(value))
		}
	}


    >>= { arg func;

        ^this.bind(func)
    }

	orElse { arg default;
		if(value.isNil) {
			^Maybe3(default)
		} {
			^this
		}
	}

	unwrap {
		^value
	}

	| { arg other;
		^Maybe3(value ?? { other.value })
	}

	asString {
		^if(value.isNil) {
			"Nothing"
		} {
			"Just " ++ value
		}
	}

	printOn { arg stream;
		stream << this.asString;
	}

	== { arg other;
		^other.isKindOf(this.class) and: { value == other.value }
	}

}