RFC: Syntax for combining patterns

Tidal v2.0 will have a lot more ways of combining patterns, an approach already trialed in strudel.

Currently strudel has 7 ways of combining the structure of two patterns (In, Out, Mix, Squeeze, SqueezeOut, Trig, Trigzero) and 25 ways of combining two values within those patterns (set, keep, keepif, add, sub, mul, div, mod, pow, _and, _or, _xor, _lshift, _rshift, lt, gt, lte, gte, eq, eqt, ne, net, and, or, func). They combine to create 7 * 25 = 175 methods for combining two patterns.

For example keepif and Out create the method keepifOut, which is actually equivalent to Tidal's struct. setIn is equivalent to Tidal's # operator. addOut is equivalent to +|, mulMix to |+| and so on.

Strudel uses these names as methods, partly because in javascript it's not possible to define new operators like # or |+. But with 175 ways of combining patterns, and more ideas to come that would add even more, maybe it's difficult to find so many operators to stand in for these names.

Here's how things look in strudel:

(we could have just used squeeze here instead of setSqueeze, as the default behaviour is to set the value)

Currently in Tidal 1.9.0 this is possible with

s "hh [~ hh] hh hh" ||> speed "1 1.2"

In the ||> operator, || indicates squeeze (squeeze cycles from the right into events on the left), and > indicates set (set values from the right). || kind of suggests squeezing (like being squashed between walls in star wars)

Is it possible to continue this logic with all this new functionality?

Another example is trig, e.g.:


The binary pattern is resetting the s pattern whenever there's a true value (reset is an easier-to-remember alias for keepifTrig).

Maybe ! could stand for this retriggering, and ? for keepif, giving the Tidal operator !?..

s "bd hh*2 bd*3 cp" !? "<t t t*2 [~ t]>"

In this case reset could be a prefix alias, i.e.

reset "<t t t*2 [~ t]>" $ s "bd hh*2 bd*3 cp"

But then we might want to retrigger a pattern with speed values like

s "bd hh*2 cp sn" !> speed "<1 1.5 [2 2.5*8]>"

Which in strudel is:

Using a prefix function for this in tidal would be a bit awkward:

setTrig (speed "<1 1.5 [2 2.5*8]>") $ s "bd hh*2 cp sn"

Using a name as infix using backticks is also possible in haskell:

speed "<1 1.5 [2 2.5*8]>") `setTrig` s "bd hh*2 cp sn"

But having a lot of backticks in patterns feels like it would get messy too.

So mostly thinking aloud here.. But interested if others have ideas for making nice syntax for combining patterns. Mostly people use # for combining patterns in Tidal but there's a lot of other different possibilities opening up here..

3 Likes

Also just thinking aloud here..

It reminds, and strikes, me that 2.0 might be a good time to formalize Tidal’s custom operator fixity. Currently [nearly] everything is the default of infixl 9, which I imagine forces use of parens and such more often than if the fixities were finely crafted. Given such fixities are already max precedence, they could likely all be customized in a non-breaking fashion, but an x.0 would be the best time for people to adjust any needed habits.

With the current ones adjusted, Tidal’s development practices could then start requiring that future operators receive finely-crafted fixity as they are added.

So, to tie the above back to the thread topic:

Tidal could introduce custom operators for some or even all of the 7 * 25 permutations, but could (just spitballing here) also or instead add 7 infix operators and 25 (unary?) functions (or 25 and 7; I haven’t really worked through the ergonomics,) all 32 of them having finely-crafted fixity.

Convenience operators for “permutations” of them would optimally be simple composition, and both their use as well as direct use of operator/function combinations should be able to do so without piles of parens and such.

I’ve not broken out an editor to test out this theory, and my work day won’t permit me to do so soon, but it seems doable for some definition of doable.

1 Like

Sounds interesting, if you could sketch an example expression that this would support that would help understand.

Meanwhile I started making all these operators:

I guess it'd be good to generate all these combinations from a script, or maybe template haskell..

Definitely a fair request, Alex, and apologies for handwaving without examples. I’m not sure if I can get to it today, but I’d definitely like to make a hands-on attempt at it, and will try to get back to you soon.

Thanks! I do fully agree btw that a 2.0 release is a rare opportunity to make any breaking changes we want, to make things more usable/consistent/whatever!

I've implemented most of this now, with a lot of search and replace. It kind of works out, but results in some very shonky operator names, e.g.:

I’ve yet to spare time for some coding for what I hope to play with, but in quickly scrolling through your latest file things are looking well on their way! The operator names are a bit unfortunate, as you mention, but probably fine for inclined users and others could use the normal function names.

I’m not 100% sure about precedence level 4 but haven’t worked through the options, and honestly don’t have the level of tidal experience to make strong suggestions. Long-time users could surely rattle off their most-hated parenthesis, $, etc. scenarios, I bet, and that could help map out precedence levels for all sorts of Tidal operators. Was the current selection of level 4 driven by something specific?

($ effectively forces precedence 0 (and parens could be seen similar at least in effect and albeit via a distinct mechanism) so tidal’s operators can probably be set very low indeed.)

re: infix, I’ve not yet had a chance to get to know this code and its intent and usage, but was a little surprised to see they’re not either left or right associative. Might you specifically mean for them to be non-associative, preventing their chained use?

It took a bit to perform toolchain updates for latest Tidal, and afterward I found the cycseq branch to be broken, so I hacked around in main, using Pattern, rather than in cycseq using Signal. Things should translate, however.

I had two goals, with the second quite dependent on the first (since the new operators require appropriate precedence also, especially in relation to other more traditional Tidal operators used around them) :

  • Infix operator precedence adjustments, which worked out quite well, eliminating parens and $ as one might expect. This is looking pretty worthwhile for 2.0, with or without the next bullet point.
  • A "7 + 25" style infix alternative/additional option for what would be 175 generated operators. This had mixed but encouraging results. Notably, it really needs the larger-scale infix operator precedence overhaul, for satisfying ergonomics.

To stop telling and start showing, in use it looks like this contrived example, with pat1 and pat5 included just for colour.

pat1 # pat2 |= Mix pat3 |. In pat4 # pat5

Above |= is set, |. is keep, and Mix and In are, well, mix and in. Operators and names can obviously be tuned as needed. And, depending on name choice, lowercase replacements for things like Mix and In are possible.

Things can also be written with one less space per usage, if desired:

pat1 # pat2 |=Mix pat3 |.In pat4 # pat5

At the cost of more wordy Mix, In, etc., one could from this have a set of 7 operators for combining structure, and that set can grow relatively independently from the set of 25 named ways to combine values, a set which can also grow relatively independently.

One interesting aspect about this work is that, because of how the types hang together, one is essentially banned from using $ in direct association with application of these structure/value combinations. $ can of course be used just a little further away, but only parens can be used within immediacy, ala:

pat1 # pat2 |=Mix pat3 |.In (doAThingTo pat4 pat5)

One cannot do this:

pat1 # pat2 |=Mix pat3 |.In $ doAThingTo pat4 pat5

I mention this only because sprinkling $ around tidal seems to be a pretty common habit for people, and this code is probably more sensitive to that than what some will have been used to.

If you think this worth pursuing further, let me know and I can keep an eye on the health of the cycseq branch and implement another proof of concept directly related to Signal and these structure / value combination behaviours.

Oh, and there is one other caveat: This syntax requires that the new operators be left-associative. To me tidal feels stylistically biased toward right associativity, and this would only affect these specific operators when used together, but I'm curious as to what @yaxu thinks about left associativity within these specific operations. Would it be okay? A problem?

Just quickly.. Yes sorry the cycseq branch is still a non-functioning sketch. I've not actually tried using any of it.. Sound.Tidal.Signal and Sound.Tidal.Sequence load in ghci though..

Yes it's great to re-appraise all this, including right associativity.. I'm not sure where infix 4 came from but I didn't ever put much thought into it.

I'm already struggling to remember what |., |= etc do.. hmm

Currently the | in |= implies In, so I think just = Mix would make more sense here, and would be equivalent of |=|. I guess you're just looking to avoid redefining the = operator though.

One interesting thing is ?|/keepifOut, is actually a redefinition of tidal's struct, and similarly |?/keepifIn is mask.

This suggests that we could potentially go through finding clearer (more declarative?) names for things, based on what they're useful for (in this case, restructuring/masking) rather than what combination of pattern/value operations they are composed of.

We can say that In, Out, Mix, Squeeze, SqueezeOut, Trig, Trigzero are all strategies for defining how the cycles of two patterns should be aligned before combining their values together.

The new sequence type on the cycseq branch has nine alignment strategies defined so far: Tidal/Sequence.hs at cycseq · tidalcycles/Tidal · GitHub

There are many more to add:
Aligning elements - Alpaca

Signals (known just as tidal patterns, pre-2.0), are aligned cyclewise, and Sequences are aligned beatwise.

By assuming one sequence takes place over one cycle, sequences easily can be turned into signals, so it's easy to align sequences with signals. However as signals can't really be turned into sequences (because e.g. they are infinitely long, and signals are finite), the result has to be a new signal.

This could complicate the picture a bit, or at least add a lot more operators.

It could be that some strategies, like squeezing, can be implemented for both signals and sequences (squeeze cycles into each event in the former, squeeze whole sequences into each step in the latter).

Darn, I hope the operators used in my examples didn’t confuse things. In my proof-of-concept code I invented different operator names, and for the examples in my message I just quickly created some more like those in the cycseq branch. No real meaning or design to them. :wink:

re: = and the like - I personally would avoid redefining existing operators unless the semantics are lawfully consistent with their normal use (at which point I’d go looking for typeclass instances to implement, if possible, rather than redefine and hide things.)

Leveraging existing tidal function names, where semantics match, does seem like a good idea. That could even happen in addition to having both named functions ala keepIfIn and their equivalent operator. And both mask and keepIfIn style named functions would be usable infix when people wish, albeit with the extra backticks.

Which reminds me that the named functions should also receive fixity declarations, with the same associativity and precedence as their operator equivalent.

Thanks for the extra details and insight.

There seems clear potential for Tidal itself to keep adding more and more over time, and also for Tidal users to add their own, likely wanting to freely combine permutations without writing a pile of their own extra named functions and/or operators.

I’m inclined toward suggesting that the fundamental model for these operations be the two-part one which affords things, usable either prefix or infix, like (again just using the crappy names from my earlier examples) :

pat2 |= Mix pat3

or

set Mix pat2 pat3

or even

(|=) Mix pat2 pat3

or

pat2 `set` Mix pat3

That largest caveat of the two-part style, when infix, is that its use can force parens in certain cases (though not unlike when using Tidal’s various prefix functions) and can (more problematically) prevent some normally-effective uses of $ to bash one’s way out of a tight spot. So, and to provide more options generally…

Then, from those building blocks, derive functions like setMix and operators like |=| for the permutations, as conveniences and to allow users to gravitate toward the prefix/infix and naming style that works for them.

And then with that in place, provide familiar alternatives with names like struct, mask, etc. where appropriate.

Where, for all of these, appropriate fixity is crafted so as to minimize parens and $ in the greatest number of common situations.

[Admittedly: The above leaves the left-associative operator aspect still unaddressed. Nearly if not all of tidal has technically been left-associative since forever, so it may not be at all a problem, but with as little as I yet know about these Signal-combining behaviours I thought I should at least raise it.]

Does all that sound about right?

Have you seen anything like this two-part style before? It seems a bit wacky, trying to look like an operator with three arguments, and might be too much like a leaky abstraction.

To think this through a bit more, it's not just in combining two patterns that these strategies come into play. It's also there when you use one pattern to modulate the transformation of another pattern.

For example, in fast 2 "bd sd", both 2 and "bd sd" are patterns. 2 could be patterned to make fast "2 3 4" "bd sd".

As things stand the structure of "2 3 3" is 'lost', i.e. the strategy for combining the two patterns is for "2 3 4" to be combined in to "bd sd". That seems a good default, but we might instead want to explore out, mix, trig, etc..

So should there also be fastMix, fastSqueeze, etc? Where we're not composing together operations on values with alignment strategies, but operations on patterns with alignments strategies?

I'm unclear what the result would be then, but might be interesting..

Or following your thinking, should it be fast (Squeeze "2 3 3") "bd sd"?

As an aside I'm wondering why I've ended up defining some of these alignment operations with applicative and the applicative-alike <* and *>, and others like a monadic join?

I guess in/out/mix are simpler in terms of alignment as you just stick one on top of the other.. squeeze/trig/trigzero you end up making a pattern of patterns first then doing the alignment when they're flattened back into patterns.. Which allows more complex alignments. Hmm!

It’s definitely just an experiment in tidal ergonomics, focused only on a) if/how infix and fixities might help disambiguate so as to reduce parens, $, etc. and b) a possible way to reduce explosions of operator/function permutations.

I doubt I’ve seen this arity of infix function at work, and if I have it’d almost surely been infix usage of a named function, rather than an operator, and even then it’d just be to help a particular case read more fluently.

So far my main take-aways have been regarding infix and named functions, almost more than the PoC:

  • Even beyond this RFC syntax, fixities can really help both operators and functions alike (for when used infix.)
  • Operator memorization, even of the “7 + 25” style, may prove difficult for people, and especially so for the “7 * 25” style, so Signal and/or Sequence combining operators should have named function equivalents, optimally with fixities for infix usage.

As for the proof of concept, it has proven doable, and feels like decent Tidal, but is admittedly pushing beyond idiomatic Haskell.

I had a few code variations running here, but the one from the examples, with the best overall tidal ergonomics (and, even then: focused on infix) actually works more like this in practice:

fast “2 3 3” Squeeze “bd sd”

Allowing for:

“2 3 3” `fast` Squeeze “bd sd”

I italicized tidal above because this argument ordering might seem odd to some, probably with respect to Squeeze. :wink:

This ergonomics experiment aside for the moment, it sounds like this has gotten your gears turning. I’m looking forward to seeing where it takes you!

I've been wondering if having an 'alignment' datatype would help.. So to combine two patterns, first you align them, then you combine the elements that align.

squeeze2 "1 2 3" "4 5" :: Alignment (Signal Int)

If somehow Alignment (Pattern a) could itself be an instance of Pattern a, then hopefully with some plumbing the above could be treated as Signal Int, by automatically turning itself into a stack post-alignment, i.e. "[1,[4 5]] [2, [4 5]] [3, [4 5]].

The 2 in squeeze2 means squeeze two things together. It could have an infix operator alias, like "1 2 3" << "4 5" or whatever. squeeze could work on a list instead, e.g. squeeze ["1 2 3", "4 5", "6 ~ 7"]

But you could also combine the aligned patterns like

combine (+) (squeeze2 "1 2 3" "4 5")

With common aliases like add = combine (+).

Again with some clever use of classes something like add might also be able to support some default, implicit alignment (like the current 'overlay and take structure from the left' behaviour) to allow add ["1 2 3", "4 5"], add2 "1 2 3" "4 5", "1 2 3" + "4 5", etc.

But beyond that default behaviour, for simplicity it could be nice to avoid the combinational explosion of operators that both align and combine elements.

So far I've talked about signals but what about sequences? It would make a lot of sense to be able to squeeze a sequence into a signal, for example. It's not really possible to squeeze a signal into a sequence, because a signal is infinite but a sequence is finite. But you could squeeze the first cycle of a signal into a sequence, if you wanted. Some more type class cleverness could make this possible.

Taking an example from above, a puzzle is what to do where we are composing operations on patterns rather than just the elements, like with this example from above:

fastSqueeze "2 3 3" “bd sd”` giving the equivalent of `"[bd sd]*2 [bd sd]*3 [bd sd]*3"

So with alignment as a separate step I guess we'd want to be able to do fast $ squeeze2 "2 3 3" "bd sd".. but then squeeze2 "2 3 3" "bd sd" is an alignment of different types. That might be a bit difficult to represent..

1 Like

Ok with some hackery on the cycseq branch

This sort of thing is possible to make an alignment (in this case, with squeezing cycles from the right into each element of the left):

"1 2" <# "3 4 5"

.. and I've defined a variant of 'fast' and 'add' that works with this:

ghci> fastA $ "1 2" <# "3 4 5"
    (0>â…™)|3
    (â…™>â…“)|4
    (⅓>½)|5
 (½>7/12)|3
 (7/12>â…”)|4
    (â…”>Âľ)|5
    (Âľ>â…š)|3
(â…š>11/12)|4
(11/12>1)|5
ghci> addA $ "1 2" <# "3 4 5"
(0>â…™)|4
(â…™>â…“)|5
(⅓>½)|6
(½>⅔)|5
(â…”>â…š)|6
(â…š>1)|7
ghci> 

Looks promising!

1 Like

One problem with this is the lack of currying. Being able to do this is great:

every 2 (fast 3) 

This is less fun to type..

every 3 (fastA . (2 #>))

We could make aliases for the different alignment behaviours though:

every 3 (fastSqueezeOut 2)
1 Like

I think this is a really good path @yaxu!
It reminds me of a Haskell course in university where we worked with embedded Domain Specific Languages.I found that making the embedded DSL in this way was very convenient because I could use brute force and pattern-match on the constructors. After some pattern emerged, I could sometimes refactor to get rid of the datatype and use functions directly. It seems like such situation might be emerging in Cycseq WIP by yaxu · Pull Request #947 · tidalcycles/Tidal · GitHub with SqueezeOut being the odd one out in _sigAppAlign.

Resources: