Fun with Joins

(Johannes Waldmann, September 2026)

A join function has type Pattern (Pattern a) -> Pattern a.

(see below for patterns in general)

There are several joins (innerJoin, outerJoin, unwrap, squeezeJoin) in the tidalcycles library, and they are used to implement other functions. Looking at tidalcycles performances, it is my impression that joins rarely occur visibly in applications. Perhaps they are little known, or they appear hard to understand (because of the nested Pattern type), or hard to write (they cannot be represented in mininotation).

Here are two examples that show why joins might be useful.

If you have more examples, or questions, regarding joins, bring them to the workshop https://www.imn.htwk-leipzig.de/LFUNK2026/, or/and post them beforehand on https://uzu.lurk.org/c/tidal/.

Separating Instruments

Let us start with a simple break beat

d1 $ s "[bd sn [~ bd] sn, hc*8]"

Now, if we want to pick different instruments, we could

d1 $ s "[clubkick sn [~ clubkick] sn, hc*8]"

and so on, but this shows a problem: we have to repeat the name (clibkick). So, we should abstract. We can use

d1 $ struct "[1 ~ [~ 1] ~]" $ s "clubkick"

but now another problem appears: we have one track per instrument

d1 $ stack
  [ struct "[1 ~ [~ 1] ~]" $ s "clubkick"
  , struct "[~ 1   ~   1]" $ s "sn"
  , struct "1*8"           $ s "hc"
  ]

so we have to write the rhythmic structure twice (or more).

We can improve this (remove the duplication) by mapping the strings in the pattern

d1 $ s $ fmap (\ i -> case i of
                  "b" -> "clubkick"
                  "s" -> "sn"
                  "h" -> "hc"
              ) ("[b s [~ b] s, h*8]" :: Pattern String)

Nice, but still not flexible enough: what if we want to add some effect (reverb, gain) to one instrument only?

We should really map each string b, s,h not to a string, but to a pattern (in the following, note the s after the arrows)

fmap (\ i -> case i of
                  "b" -> s "clubkick"
                  "s" -> s "sn"
                  "h" -> s "hc"
              ) ("[b s [~ b] s, h*8]" :: Pattern String)

Now the whole expression has type Pattern (Pattern ValueMap), and that’s where we need to join (here we use unwrap, which is one of the variants), so we get Pattern ValueMap that we can send to the back-end.

d1 $ unwrap $ fmap (\ i -> case i of
                  "b" -> s "clubkick"
                  "s" -> s "sn"
                  "h" -> s "hc"
              ) ("[b s [~ b] s, h*8]" :: Pattern String)

Now we can indeed apply modifications by instrument, e.g.,

d1 $ unwrap $ fmap (\ i -> case i of
                  "b" -> s "clubkick"
                  "s" -> s "sn" # orbit 1 # room 1 # size 0.8
                  "h" -> s "hc"
              ) ("[b s [~ b] s, h*8]" :: Pattern String)

Modification can be patterned: the gain of the hihat is lower in the first half of the measure

d1 $ unwrap $ fmap (\ i -> case i of
                  "b" -> s "clubkick"
                  "s" -> s "sn" 
                  "h" -> s "hc" + gain "0.8 1"
              ) ("[b s [~ b] s, h*8]" :: Pattern String)

This example can be used to show the difference to squeezeJoin, which has identical type :: Pattern (Pattern a) -> Pattern a but different semantics: each event of the outer pattern contains an inner pattern that will be squeezed to the extension of the outer event. We now get 16th on the hihat.

d1 $ squeezeJoin $ fmap (\ i -> case i of
                  "b" -> s "clubkick"
                  "s" -> s "sn" 
                  "h" -> s "hc" + gain "0.8 1"
              ) ("[b s [~ b] s, h*8]" :: Pattern String)

When we put *2 on the bass drum,

d1 $ squeezeJoin $ fmap (\ i -> case i of
                  "b" -> s "clubkick*2"
                  "s" -> s "sn" 
                  "h" -> s "hc" + gain "0.8 1"
              ) ("[b s [~ b] s, h*8]" :: Pattern String)

we get two 8th , then two 16th, since the first b event has extension 1/4, while the second b even is just 1/8.

Modifying patterns, Randomly

If we have any pattern p, we might use it in the forms of id p, rev p, fast 2 p, where each expression id, rev, fast 2 has type Pattern a -> Pattern a.

Now we want to pick a function Pattern a -> Pattern a from a list, randomly. We get this type

choose [id, rev, fast 2] :: Pattern (Pattern a -> Pattern a)

We use applicative combination (we only want to see the type of it all, so we do use undefined for the inner pattern)

choose [id, rev, fast 2] <*> pure (undefined :: Pattern a)
  :: Pattern (Pattern a)

now again, we apply some join function, to get Pattern a

Here is an application of both ideas (separating instruments, randomizing modifications): code, audio.

Another join you’ll find (from 4 min onwards) at Haustechnik where the purpose is to transmit the common base note to bass and piano.

Patterns

The basic concept of https://tidalcycles.org/ (McLean) is Pattern a, which is a set of events, where each event has a value (of type a), and an extension (an interval of time). Time is unbounded, and a pattern may have infinitely many events, but in each finite interval of time can contain only finitely many events.

(Application: a ValueMap describes a parameter object, consisting of instrument name, note number, effects settings, etc., that can be sent to an audio back-end, and Pattern ValueMap describes a piece of music, as a set of such events)

Elementary patterns are

  • silence: no events
  • pure x: for each integer i, an event with extension (i, i+1) and value x (always the same value).

A pattern can be modified by

  • _ : Rational -> Pattern a -> Pattern a (keep the values, modify time: fast speeds up, rotL shifts)
  • fmap : (a -> b) -> Pattern a -> Pattern b (transform the values, keep the time structure)

Patterns can be combined

  • in parallel: stack :: [ Pattern a ] -> Pattern a: take the union of the events (don’t change values or extensions)
  • interleaving: cat :: [Pattern a] -> Pattern a: keep values but change time: imagine each pattern as a tape machine, then in turn (round robin), run each machine for 1 unit of time, while the others are waiting (more explanation)
  • applicatively: (<*>) :: Pattern (a -> b) -> Pattern a -> Pattern b: functions producen by first pattern will be applied to values produced by second pattern. Here,fmap is the special case where the function is constant: fmap f p = pure f <*> p.