(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/.
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.
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.
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 eventspure 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
stack :: [ Pattern a ] -> Pattern a: take
the union of the events (don’t change values or
extensions)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)(<*>) :: 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.