Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composable functions #390

Merged
merged 20 commits into from Feb 16, 2023
Merged

Composable functions #390

merged 20 commits into from Feb 16, 2023

Conversation

yaxu
Copy link
Member

@yaxu yaxu commented Jan 21, 2023

Giving up on #368 (patterns that behave like composable functions depending on the context), for lack of javascript type inference.

Instead trying functions-as-functions and patterns-as-patterns. Single-argument functions of patterns get dressed up with all the registered pattern method names, which are simply there to do function composition (which are also then dressed up) or finally apply the composed of functions to a pattern.

So far these are equivalent:

sequence(3,4).fast(2).rev().fast(2).
fast(2).rev().fast(2)(sequence(3,4)).

@yaxu
Copy link
Member Author

yaxu commented Jan 21, 2023

Continuing discussion from #368 (comment) ..

Ah I was confused, set.n(3) isn't a thing, but set(n(3)) works.. with n(3).keep.out an equivalent.

set replaces plain values in patterns, and does a union replacing values on the left for patterns of objects.

But set(n(3)) isn't much easier to read/type than x => x.n(3).. So maybe we just add all the controls as methods to all the alignment functions so that set.n does work..

To fully replace a pattern you could do:

s("bd sd").every(3, constant(s("cp")))

constant is now curried on this branch. It could have a better name though! Replace?

Anyway, this sort of thing works now:

s("bd sd").every(3, fast(2).rev().iter(4).out(s("bd*5")).fast(2))

@yaxu
Copy link
Member Author

yaxu commented Jan 21, 2023

Ok instead of confusing things with munging set, I made a new thing called a hitch, which acts like haskell's $ when partially applied, and really only exists for eliminating parenthesis.

So s("bd sd").every(3, hitch.fast(2).n(3)) now works.

@yaxu
Copy link
Member Author

yaxu commented Jan 22, 2023

Finally got some time to think this through a bit more.

s("bd sd").n("3 4") is shorthand for s("bd sd").set.in(n("3 4"))

This is a little strange, in that we are using method calls both as methods (like .fast(3), and as shorthand for connecting two patterns, where . looks like an operator for combining values rather than calling a method

If we keep that (and I think we clearly should), then I think it follows that we should also support the (currently non-working) s("bd sd").set.in.n("3 4")

If we fix that, then set.in.n("3 4") and the equivalent shorthand set.n("3 4") and in.n("3 4") should start composing into the equivalent of x => x.n("3 4")

This means we can do s("bd sd").every(3, set.n("3 4").fast(2))

We can also do s("bd sd").every(3, fast(2).n("3 4")), avoiding the set., but the resulting pattern is a bit different

I'm still unsure whether s("bd sd").every(3, n("3 4").fast(2)) without the set is possible, but given the above, for the end-user it would be surprising if it didn't work.

The problem is that the second parameter to every is a pattern of objects, but could be a pattern of functions to apply to s("bd sd"). Because patterns are functions, and because in strudel there is no strict typing, we don't know until we look.

Ideally, s("bd sd").every(3, sequence({n: 3}, fast(2))) would work. I don't really see why that wouldn't be possible. ll have a go.

So TODO:

  • Allow alignment functions to take control pattern arguments as methods e.g. set.n(3)
  • Try to support things like s("bd sd").every(3, n(3)), s("bd sd").apply(sequence(fast(2), n(2))) etc

@yaxu
Copy link
Member Author

yaxu commented Jan 22, 2023

Try to support things like s("bd sd").every(3, n(3)), s("bd sd").apply(sequence(fast(2), n(2))) etc

This was easy to do but the problem is that n(3).fast(2) is resolved before merge so the whole pattern wouldn't get sped up. So I guess people would need to remember to do set.n(3).fast(2) or hitch.n(3).fast(2) for that.

I feel like I'm going around in circles on this one..

@yaxu
Copy link
Member Author

yaxu commented Jan 23, 2023

Ok according to the principle of least surprise..

I think in this one, the s("cp").fast(2) should completely replace the s("bd sd") pattern.

s("bd sd").n("3 4").every(3, s("cp").fast(2))
// shorthand for:
s("bd sd").n("3 4").every(3, constant(s("cp").fast(2)))

So end-users have to learn the hopefully fairly straightforward principle that if the pass a pattern rather than a function, the pattern replaces the pattern they're calling it on.

Of the alternatives:

s("bd sd").n("3 4").every(3, x => x.set.in(s("cp")).fast(2))
s("bd sd").n("3 4").every(3, x => x.set.in(s("cp").fast(2)))

The first one is not really possible to implement, and the second is probably not what they want in most cases.

edit argh but we can't do this, as we also want to support patterns-of-functions..

So maybe best to just keep things super simple - raise an error if an object is found where a function is expected.

@felixroos
Copy link
Collaborator

So end-users have to learn the hopefully fairly straightforward principle that if the pass a pattern rather than a function, the pattern replaces the pattern they're calling it on.

sounds reasonable. We could also simplify passing functions to transpile:

s("bd").lastOf(2, set.speed(2))

into

s("bd").lastOf(2, x=>x.speed(2))

or alternatively, find a way to implement set to support that.

edit argh but we can't do this, as we also want to support patterns-of-functions..

shouldn't this already work? at least this works atm:

s("bd").lastOf(3, cat(x=>s("sd"), x=>x.speed("2")))

when lastOf receives the param, the pattern is already unwrapped, so it either gets x=>s("sd") or x=>x.speed("2").
With sugar, it could be written as

s("bd").lastOf(3, cat(s("sd"), set.speed("2")))

in that case, lastOf will receive s("bd") (?) or set.speed("2"). So it needs to check if the param is a pattern or a function (assuming set. resolves to x=>x.)

So I guess people would need to remember to do set.n(3).fast(2) or hitch.n(3).fast(2) for that.

I don't think that would be a problem. This applies fast before merge:

s("bd").lastOf(3, s("sd").fast(2))

(link)

This applies fast to the outer pattern:

s("bd").lastOf(3, set.s("sd").fast(2))

(link)

In this case, the result is actually the same, but I think there should be cases where it makes a difference..

@yaxu
Copy link
Member Author

yaxu commented Jan 27, 2023

or alternatively, find a way to implement set to support that.

This is possible without transpilation. .set is already a function created on-the-fly via a getter, so we can add a function for each of the controls to that.

With sugar, it could be written as

s("bd").lastOf(3, cat(s("sd"), set.speed("2")))

I don't think so, because strudel has no way to tell whether s("sd") is a pattern of functions or a pattern or values, so doesn't know whether to add the sugar or not.

in that case, lastOf will receive s("bd") (?) or set.speed("2"). So it needs to check if the param is a pattern or a function (assuming set. resolves to x=>x.)

But if it's a pattern, we don't know if it's a pattern of functions or a pattern of (in this case) objects.

s("bd").lastOf(3, cat(cat(rev, fast(2)), set.speed("2")))

In the above, cat(rev, fast(2)) looks exactly the same as s("sd"), but we don't want x => cat(rev, fast(2)).

So I guess people would need to remember to do set.n(3).fast(2) or hitch.n(3).fast(2) for that.

Some confusion here - this was from an earlier point in my train of thought, where I was still trying to make n(3) behave as set.n(3), rather than x => n(3).

Currently I think

  • apply(n(3)) should be an error because that's a pattern not a function. This mirrors behaviour in tidal, but annoyingly would be a runtime error rather than a typecheck error. We might generate the first 'x' cycles of a pattern on evaluation to try to catch such errors as early as possible so that the current pattern isn't replaced
  • apply(set.n(3).fast(2)) or apply(hitch.n(3).fast(2)) or apply(add.squeeze.n(3,2).fast(2)) composes a function to apply to the outer pattern
  • apply(x=>n(3).fast(2)) replaces the outer pattern and apply(set(n(3).fast(2))) speeds up the inner pattern before applying it

@yaxu
Copy link
Member Author

yaxu commented Feb 16, 2023

Summary of changes:

  • Made unary functions composable, including controls. So e.g. s("bd sd").every(3,fast(2).iter(4).n(4)) works the same as s("bd sd").every(3,x => x.fast(2).iter(4).n(4))
  • Made operators/alignments composable too, so s("bd sd").every(3, set.squeeze.n(3, 4)) works
  • Patterns are not treated as functions, so s("bd sd").every(3, n(5)) is an annoying runtime error. s("bd sd").every(3, set.n(5)) does work though.

Other minor changes:

  • standardised alignment 'squeezeOut' as lowercase 'squeezeout'
  • made firstCycleValues turn haps sorted in order of 'part'

@yaxu yaxu merged commit cbae355 into main Feb 16, 2023
@yaxu yaxu deleted the composable-functions branch February 16, 2023 23:15
@felixroos
Copy link
Collaborator

great!

@yaxu yaxu changed the title Another attempt at composable functions - WIP Composable functions Feb 17, 2023
felixroos added a commit that referenced this pull request Feb 27, 2023
felixroos added a commit that referenced this pull request Feb 27, 2023
Revert "Another attempt at composable functions - WIP (#390)"
@roipoussiere
Copy link
Contributor

Maybe I don't understand how it works but this:

s("bd sd hh").sometimes(set.speed(-1))

produces an error in the Strudel REPL (set.speed is not a function).

@felixroos
Copy link
Collaborator

felixroos commented Jun 18, 2023

produces an error in the Strudel REPL (set.speed is not a function).

the above snippets were just hypothetical set is currently just a function, but it could get all existing controls as member functions to to allow that syntax. It currently works like this: https://strudel.tidalcycles.org/?YChrbNmwD8R9

edit: did not read properly, not realizing that this was the actual PR where this was added (before it was reverted again).

@yaxu
Copy link
Member Author

yaxu commented Jun 18, 2023

Oh I'm pretty sure with this PR (#390) set would have got all the controls added. I remember a lot of it was reverted due to performance issues, but probably set.speed and friends should have survived?

edit Hmm I'm remembering the issue now.. Yes I guess this needed all the composability that didn't work out :/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants