tidalcycles / strudel

Web-based environment for live coding algorithmic patterns, incorporating a faithful port of TidalCycles to JavaScript
https://strudel.cc/
GNU Affero General Public License v3.0
692 stars 122 forks source link

Curried controls ? #264

Open yaxu opened 2 years ago

yaxu commented 2 years ago

Currently this sort of thing doesn't work:

s('bd').every(3,n(2))

This feels surprising, s('bd').x(2), x(2, s('bd')) and s('bd').every(3, x(2)) work when x=fast, but not when it's a control like n.

I don't think we can fix this with currying. Maybe it's up to every and friends to check whether its last argument is a function or a pattern, and in the latter case combine it with setIn..

That would imply support for e.g. s('bd').every(3,setOut.n(2)) as well.

felixroos commented 2 years ago

yep this should work in the ideal case.. related: https://github.com/tidalcycles/strudel/issues/36

I don't think we can fix this with currying. Maybe it's up to every and friends to check whether its last argument is a function or a pattern, and in the latter case combine it with setIn..

so far I tried this: https://strudel.tidalcycles.org?fpNMjYivvAhZ but it breaks non controls like add etc.. I think there is a solution with currying but it's really hard to wrap a monkey brain around it.. if add and friends should remain curried functions, we need to find a way to give them all pattern methods to support chaining like

n("c3").every(3,add(2).fast(3))
// n("c3".every(3,x=>x.add(7).fast(3))) // <- this works

the problem is that we also need to wrap each supplied pattern method in a function that expects a pattern as its last argument. I tried this with the overload param of our curry function but no luck when I last tried it

felixroos commented 2 years ago

.. the problem with using set / setIn is that it also ignores any temporal transformations like fast: https://strudel.tidalcycles.org?2ZZjVuty3iy4

yaxu commented 2 years ago

True I guess not fully ignoring, just not applying the fast to the whole pattern, which in some circumstances you wouldn't want.

I've been working on https://github.com/tidalcycles/strudel/discussions/252 as well on the general-tidy branch but the composable stuff makes things more complicated.

What do you think about this solution for the issue in hand: https://github.com/tidalcycles/strudel/commit/ffd4d437cfc2c9a02bd27fe3adb119cbc08791cd

yaxu commented 2 years ago

To support this:

add(2).fast(3)

Would it help for add(2) to return a pattern of functions? Then this expression would automatically 'work'. Then "1 2".lastOf(2, add(2).fast(3)) would combine the two patterns, find a function in one of them, and apply it to the value in the other. If you wanted the result sped up you'd still have to do "1 2".lastOf(2, x => x.add(2).fast(3)), or "1 2".lastOf(2, add(2)).lastOf(2, fast(2)), but that makes sense to me.

yaxu commented 2 years ago

So lets say add is a 'getter' that returns a function with a properties for each way of aligning source and target events before adding them, add.in, add.squeeze, with the add function an alias for the add.in default.

But then add(2) can't just be a pattern of functions, as it also says how that pattern should be combined with something else. This could be some metadata..

yaxu commented 2 years ago

Taking this a bit further..

Currently we have patterns like pure(3) and functions like fast(2).. but a pattern is a function of state, so really they're both functions. So why not make things so pure(3)(state) works?

Then we can have polymorphic functions that do different things depending on the input. E.g. add("3 4") could either be a function that takes a pattern as input, and returns a pattern as output like add("3 4")("10 20"), or it could be a pattern of functions, like add("3 4").fast("2 3").squeeze("10 20").

This implies standardising on the 'varargs as sequences' behaviour, so the above could be written as add(3, 4).fast(2, 3).squeeze(10, 20), where add(3, 4) doesn't result in 7 - but add(3)(4) would.. A bit confusing, but maybe better to standardise on this single behaviour.

add("3 4").mul("10 20") would then be composing two patterns of functions together. This needs some fiddling with the default 'set' behaviour. I'd propose:

I'm not 100% sure whether both the second and third examples should apply the function, maybe just one of them should, and the other should overwrite as per the first one..

This becomes useful in stuff like

"30 40".every(3, add("1 4").div(2).every(2,slow(2)))

This leads to a big question, whether that slow(2) should work on the pattern of functions and only speed up the "1 4", or whether it should work on the whole pattern, e.g. "30 40" after every(3, add("1 4").div(2)).. In other words, should everything be a pattern, including patterns of functions, or should everything be a function, including patterns?

I'm leaning towards the former as a general principle, but where `add("3 4")("4 5") works as syntactic sugar..

felixroos commented 2 years ago

What do you think about this solution for the issue in hand: https://github.com/tidalcycles/strudel/commit/ffd4d437cfc2c9a02bd27fe3adb119cbc08791cd

I think not applying transformations (like fast) adds a completely new behavior that is confusing. It should help to separate between design goal and implementation strategy. The design goal is pretty clear for me:

n("c3").every(3,add(2).fast(3))

should behave exactly like currently

n("c3".every(3,x=>x.add(7).fast(3))) 

so the goal is to just get rid of x=>x. while keeping the same behavior. This way, the conditional transforms follow the same intuition as all other patterns:

n("c3").every(3,add(2).fast(3))
// intuition: every 3 cycles, the transformation (2nd argument) will be "applied":
n("c3").add(2).fast(3) // this is how the pattern looks like every 3rd time

imo we should prevent any behavior that deviates from this intuition, e.g. fast does not work etc..

(jumping ahead to your last comment)

This leads to a big question, whether that slow(2) should work on the pattern of functions and only speed up the "1 4", or whether it should work on the whole pattern, e.g. "30 40" after every(3, add("1 4").div(2))..

See: https://strudel.tidalcycles.org?o0cWDkvmr86N It looks/sounds like the current behavior is applying the slow to the whole pattern:

cycle 0: "30 40".add("1 4").div(2).slow(2)
[hap] 0/1 -> 1/1: note:15.5 = (30+1)/2
cycle 1: "30 40"
[hap] 1/1 -> 3/2: note:30
[hap] 3/2 -> 2/1: note:40
cycle 2: "30 40" 
[hap] 2/1 -> 5/2: note:30
[hap] 5/2 -> 3/1: note:40
cycle 3: "30 40".add("1 4").div(2)
[hap] 3/1 -> 7/2: note:15.5 = (30+1)/2
[hap] 7/2 -> 4/1: note:22 = (40+4)/2
cycle 4: "30 40"
[hap] 4/1 -> 9/2: note:30
[hap] 9/2 -> 5/1: note:40
cycle 5: "30 40"
[hap] 5/1 -> 11/2: note:30
[hap] 11/2 -> 6/1: note:40

the next cycle loops back to the beginning..

It would still work to apply the slow only to the inner add pattern: https://strudel.tidalcycles.org?IEUwGhtg5vWj I think, applying the slow to both inner patterns (add and div) does not work without duplication..

In other words, should everything be a pattern, including patterns of functions, or should everything be a function, including patterns?

I am not sure if this is really a a vs b situation, because we have actually have 4 different ideas at play:

  1. design: transformations should be applied globally
  2. implementation: functions as patterns
  3. design: transformations should be applied locally
  4. implementation: patterns of functions

so maybe 1 is also possible with 4 etc.. Also, there might be a way to support both global and local transformation applications.

Now to talk about implementation: I also think it might be handy to have patterns of functions, because we might have more control over the control flow :>

Would it help for add(2) to return a pattern of functions? Then this expression would automatically 'work'. Then "1 2".lastOf(2, add(2).fast(3)) would combine the two patterns, find a function in one of them, and apply it to the value in the other. If you wanted the result sped up you'd still have to do "1 2".lastOf(2, x => x.add(2).fast(3)), or "1 2".lastOf(2, add(2)).lastOf(2, fast(2)), but that makes sense to me.

design wise, I think the more common thing should be easier to write than the less common thing. Not too sure, but I think the more common thing is to apply globally.. also it seems a little crazy to keep the x=>x. syntax while the syntax without it behaves differently.

Currently we have patterns like pure(3) and functions like fast(2).. but a pattern is a function of state, so really they're both functions. So why not make things so pure(3)(state) works? Then we can have polymorphic functions that do different things depending on the input.

sounds useful, but implementation wise I think you cannot make a class instance (Pattern) behave like a function. It only works the other way around: implementing Pattern as a function and adding all the methods afterwards, which is pretty much a rewrite (also with new problems to solve).

I'm leaning towards the former as a general principle, but where `add("3 4")("4 5") works as syntactic sugar..

yup.. but that would only be possible with transpilation.

Another idea would be to just transpile

n(42).every(2, add("2 3"))

to

n(42).every(2, x=>x.add("2 3"))

.. but of course this would mean global applications are the default.

That's enough text for today, but I won't leave before asking the standard question: How is tidal doing it?

I am still not 100% sure about all of this, and it is likely that I missed something..

yaxu commented 2 years ago

I always find it difficult to separate design and implementation in my head, I tend to work with an implementation (real or imagined) as material in the design process.

But that's a real shame that class instances can't be functions. :/

n("c3").every(3,add(2).fast(3))

should behave exactly like currently

n("c3".every(3,x=>x.add(7).fast(3))) 

Maybe but I think it's worth thinking about it a bit more. It does seem like an intuition, but that just comes down to learned assumptions really.

e.g. I don't have too much of a problem squinting my eyes so that in the following, add("2 4") makes a pattern of adding and fast(3) speeds up that pattern of adding.

"c3*6".every(3,add("2 4").fast(3))

Then this sort of thing makes sense, because everything is a pattern:

"c3*4".every(3,stack(add("2 4"), mul("3 1")).fast(3))
"c3*4".every(3,stack(add("2 4"), mul("3 1").fast(3)))

.. so pushing towards patterns rather than functions seems to open up new possibilities.

But if I say everything is a pattern, does that make fast(3) a pattern? Currently it's a pattern tranformation function (pattern in, pattern out). But.. it could be a pattern of pattern transformation functions. Then we could do

"c3*6".every(3, sequence(fast(3), slow(2.25)))

Hm!

Maybe then every(3, fast(2).add("2 4")) raises an error, as you can't really add numbers to a pattern of functions.. Unless from the context add("2 4") is turned into a pattern of functions too, and the composition is done inside the patterns.

If we want to duplicate tidal behaviour as much as possible though, then yes supporting haskell-like currying and composition as much as possible is probably the way forward.

(+ "3 2") . fast(2) -- return a function to add to the pattern, then speed up the result
fast(2) . (+ "3 2") -- speed up the pattern, then add to the result of that
(+ (fast 2 "3 2")) -- speed up the numbers, before adding to the pattern

currently same as

x => x.add("3 2").fast(2) // add to the pattern, then speed up the result
x => x.fast(2).add("3 2") // speed up the pattern, then add to the result of that
x => x.add("3 2".fast(2)) // speed up the numbers, before adding them to the pattern

with an implied lambda, could be reduced to

add("3 2").fast(2) // add to the pattern, then speed up the result
fast(2).add("3 2") // speed up the pattern, then add to the result of that
add("3 2".fast(2)) // speed up the numbers, before adding them to the pattern

I'm wondering though if taking advantage of more fluid types opens up alternative possibilities that fit with javascript better.

The last one above feels a bit icky to me because . means something quite different in javascript and simulating composition and currying becomes a huge tangle.

design wise, I think the more common thing should be easier to write than the less common thing.

Or playing devil's advocate, the easiest thing to write will very likely become the more common thing as a designed affordance.. and if that's the least common thing in some wider music culture, maybe that's good for supporting less explored corners of music. :)

I'll probably explore a bit more anyway, and see if I can get something interesting working.

felixroos commented 2 years ago

Or playing devil's advocate, the easiest thing to write will very likely become the more common thing as a designed affordance.. and if that's the least common thing in some wider music culture, maybe that's good for supporting less explored corners of music. :)

hehe there is def truth to that, reminds me of "we shape our buildings; thereafter they shape us." though I think we are already quite out there :)

felixroos commented 2 years ago

ref https://github.com/tidalcycles/strudel/discussions/82#discussioncomment-4223914

yaxu commented 2 years ago

I wonder if we could 'do both'

add("3 4") is sequence(\x => x+3, \x => x+4), so is a pattern with all the methods.

But it also has an extra property called curried or something that stores the curried pattern method:

sequence(\x => x+3, \x => x+4).curried(x => x.add("3 4"))

Then whenever a method is called on that pattern, that method checks if its called on a pattern with a curried value. If it does, then functional composition happens. This behaviour would be added to all methods via some wrapper.

so then add("3 4").iter(4) would be a new pattern with a curried property of x => x.add("3 4").iter(4). Higher order functions like every would check .curried for a function to apply so that .every(3, add("3 4").iter(4)) works.

This is mostly a hack to do automatic currying while avoiding having to construct a function with all the pattern methods. But there might well be some cases where it's useful for the pattern to be treated as an actual pattern..

I.e. in the following pat (or something better named) could tell the iter method to apply the transformation to the pattern of functions, rather than doing composition.

add("3 4").pat.iter(4)

Maybe that could be the equivalent of:

sequence(\x => x+3, \x => x+4).iter(4).curried(x => x.add("3 4".iter(4)))

Not sure how easy that would be to implement, though..

Does this make any sense?

yaxu commented 1 year ago

I had a quick play with this again: https://github.com/tidalcycles/strudel/compare/main...composable

Similar to the above, I thought a control could get a 'compose' property added, which is the function version of itself.

Then higher order functions like lastOf can check for that property and use it if present.

However if you pass a pattern as a parameter to lastOf, it will get unwrapped into a value by the magic in register.

So somehow register needs to know that when a function is expecting a function, and it is being passed a pattern of values, not a pattern of functions, that it should therefore not unwrap that pattern. Pretty tricky without strict types.

edit It does work with the non-patternified version though:

s("bd sd").n("0")._lastOf(3,n("2"))
yaxu commented 1 year ago

Ok with some hackery to the register function this is starting to work: https://github.com/tidalcycles/strudel/commit/c64485db99984e0c1259caa914a5452162aa6c70

s("bd").lastOf(3, speed(2))and s("bd").jux(speed(2)) work

s("bd").jux(speed(2).fast(2)) doesn't work, that would need some more hackery to do the composition down the chain..