Flexible binds for combining musical patterns

I’m working on the TidalCycles project for making music with Haskell, and trying to rejig things so they’re more flexible.

Tidal is all about representing musical patterns that can be combined in a variety of ways. There are two instances of a Pattern class, broadly Signal for representing patterns as functions of time (in a FRP manner), and Sequence for representing them as lists of events with durations.

There are many ways of combining patterns, particularly with sequences. You can align two sequences in a variety of ways (currently: justifyleft, justifyright, justifyboth, expand, truncateleft, truncateright, truncaterepeat, rep, centre, squeezein, squeezeout), and after that combine them with different strategies that preserve the structure from either the left- or righthand sequence.

However, Haskell seems to work against the idea that there can be different ways of combining things, where a monad only has one bind. If you want different behaviour you have to define a whole different type (as with ZipList). This approach seems too unwieldy for a code interface for making music.

I’m trying to work around this by adding extra fields to the datatypes that say how a pattern should be combined with another. The >>= bind can then use this information for deciding how to combine two sequences (or signals), and the <*> can use it for deciding how sequences should be aligned before being combined.

With this I can e.g. do the equivalent of (rep [1,2,3]) + [10 20] to repeat both sides to the lowest common multiple [11, 22,13, 21,12, 23], or e.g. (expand [1,2,3]) + [10 20] to expand the shortest sequence to the duration of the longest, which results in fragments of events (which is a bit hard to visualise in text).

Hopefully this isn’t breaking too many monad laws…

Anyway I am a self-taught Haskell programmer and would really appreciate some feedback on this approach. I think this should be a common problem, are there common solutions?

3 Likes

o_- …I 've seen this before:

…if that’s (primarily) the reason why you’re now looking at implementing the monadic interface: I was using Monad as an analogy; I didn’t intend that to be a suggestion or advocacy - apologies for any confusion about that.

Ah yes this is a continuation of that work. Your advice in that first post was extremely helpful and I’ve run with that. I’ve renamed the types, so that the class is now called Pattern, and the instances are called Signal and Sequence. This all seems to be going very well.

Your second post isn’t the reason why I’m working with monads. Tidal’s pattern representation always has leaned heavily on applicative and monad instances and I’m extending that work.

One motivation for all this is that in TidalCycles, just about function argument can be a pattern. For example the internal function _fast :: Pattern p => Time -> p a -> p a ‘speeds up’ a given pattern by a given time factor. A monadic bind used to lift it into fast :: Pattern p => p Time -> p a -> p a, where the time factor is also ‘patterned’.

Which bind is used is decided by looking at the lifted pattern:

This results in a really flexible system where you can not only give a pattern of factors to speed up another pattern, but also specify how these two patterns are aligned and combined in order to come up with a result.

I hope this makes some sense!

Hrm:

…question: is that part of TidalCycles acting like a runtime system for the rest of the project?

Hmm I’m not sure what you mean by runtime system in this context.

This part is for making functions for an end-user programmer (live coder) to make music with in this sort of situation:

So maybe the concept of runtime doesn’t really apply, or at least the programmer is part of the runtime.

Alright, I’ll consider my initial reaction a “red herring” arising from my ol’ implementor experience :-) …back to the current topic!

The lack of flexibility regarding (>>=) has lead to other abstract interfaces e.g. arrows. But not all combinator libraries need this style of interface:

Of course, it could also be that no single approach is sufficient - you may need to combine them to achieve your goals.