Syntax tips for stitching Tidal functions together

Before I am asking for more let me say that the help I got from this forum is excellent and very friendly. Alex's weekly course videos and his other videos as well as the recorded frequently faq sessions (also with Lucy) are great. And last but not least I really appreciate the userbase and all its help documents; finally Hoogle can be of help even if this can be much to digest for a beginner. Many, many thanks for that!

In another posting I did mention that I also found Carsten Heisterkamp's introduction quite helpful. Especially the list of guidelines to explore and first and foremost to combine Tidal functions is something which for me - and I suppose any beginner - is potentially fruitful. Under the heading "Composing functions the right way" he writes:

Tidal really gets fun when you learn how to write your patterns and manipulate and alter them by intuitively sticking functions together without being interrupted by the compiler and searching for bugs in your code. This is most of the time the case, either because of typos and most of the time because we tried to combine functions with signatures which do not fit.

I am quite aware that the internals of type signatures and related language internals are not everyone's cup of tea but I really like to dive in and found Heisterkamp's argument quite convincing (for me). This is where I started a few months ago. Besides the mentioned article there are some resources about expressions like $ and # and I guess I meanwhile have consumed all of it (which obviously does not mean: understood all of it). I think I now can interpret type signatures and I have an idea about how these can help to find out how to combine Tidal functions.

On the other hand I still have a lot of questions, am confused especially about syntax and find myself using trial-an-error-methods (interesting to explore a programming language solely with empirical methods like it was nature :wink: ), which is not wrong but I'd appreciate also a logically guided and systematic way.

So here are some very helpful yet also confusing hints from the mentioned article (see the chapter titled "Composing functions the right way"); this is step 4 and 5 he provides with reference to a reasonably complex example:

"4. If you want to use a function that returns a pattern inside a function that takes a pattern of type ControlMap, you need to evaluate it first with the bracket () notation, like juxcut (# speed 2)"

"5. Vice versa if you want to use a function which can not be used with the # because they don´t take but return pattern inside a function that takes a pattern you also need to evaluate them first with the bracket notation ().For example # pan (slow 4 $ sine ) or # gain(choose [0.5 0.75 1])

The example code is:

d1 $ slow 4
   $ spaceOut (map (1/) [1, 1.2 .. 10])
   $ sometimesBy 0.66 (rev)
   $ rarely (juxcut(# speed (choose[0.33, 0.5, 0.66, 0.75, 1.5, 2 ]))) 
   $ sound “bd*4”
   # n (choose[1, 3, 5, 7])
   # pan (slow 4 $ sine )
   # orbit 0

Now looking at the type signatures tells me:

juxcut :: (Pattern ControlMap -> Pattern ControlMap) -> Pattern ControlMap -> Pattern ControlMap

and

speed :: Pattern Double -> ControlPattern

In this respect hint #4 makes some sense (leaving also some darkness):

As far as I have understood a ControlMap is or can be a subunit of a control pattern and serves to directly control sound resp. Supercollider (pardon me if this is very simplistic but it is what I have grasped so far). In really simple words: As speed supplies a pattern and juxcut expects one, the # function is necessary to combine both (which does not really seem to be the correct explanation now that I am reading my text again...).

There are other similar cases such as every I e. g. found in an example from a Kindohm interview:

d1
  $ every 3 (0.5 <~)
  $ every 4 (chop 4)
  $ every 5 (# speed "1.5 -1 0.5")
  $ every 6 (# crush 2)
  $ every 7 (rev)
  $ stack [ sound "bd(3,8)", sound "cp*2"]
speed :: Pattern Double -> ControlPattern
crush :: Pattern Double -> ControlPattern
pan :: Pattern Double -> ControlPattern

The type signature of every is:

every :: Pattern Int -> (Pattern a -> Pattern a) -> Pattern a -> Pattern a

So this is obviously a case where the Heisterkamp rule #4 applies but this rule does not seem to be defined as general as it should or could: every does not expect a 'ControlPattern' but - patterns of type int and a and a function.

There are other functions where the # is not necessary such as

$ every 7 (rev)

rev :: Pattern a -> Pattern a

and

$ every 2 (chop 4)

chop :: Pattern Int -> ControlPattern -> ControlPattern

So I am sure this is all explainable in a very logic way. It is just that I can't see it with the knowledge I have been able to gain so far.

Finally I am not really sure how the rule #5 fits into the image. Actually I see the reference within the code example but I can't really map the rule with the type signatures:

# pan (slow 4 $ sine )

slow :: Pattern Time -> Pattern a -> Pattern a
pan :: Pattern Double -> ControlPattern

It might be, that my lack of understanding rises from the fact that I don't really understand the conceptual differences between ControlMap, ControlPattern and some other Pattern.

I really do hope that I was able not only sound like I am complaing about my confusion but also to somehow give it a form that someone more experienced can pick up the tread and help to clear this up. It is somehow quite difficult to come up with clear questions (usually once you have a clear question the answer is not far away).

I guess my main question is, whether it is possible to extend the already awesome resources that exist with a document titled something like "Syntax tips for stitching Tidal functions together" (sorry, English is not my first lanuage).

I reckon this would not only help me but also other beginners.

Anyway, if you have come to this point: Thanks for reading these lengthy notes!

1 Like

Hi.

You took great care, and length, to describe the situtation. Much appreciated.

You ask about (concrete) syntax (= what you write/see), but a discussion of abstract syntax (= the tree shape that is described by concrete syntax), and static semantics (= types) should be helpful.

This is about expressions built from operators (I am showing a ghci session)

:i ($)
($) :: (a -> b) -> a -> b 	-- Defined in ‘GHC.Base’
infixr 0 $

and

:i (#)
(#) :: Unionable b => Pattern b -> Pattern b -> Pattern b
  	-- Defined in ‘Sound.Tidal.Core’

Since # has no explicit "fixity", the declaration infixl 9 # is assumed (https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-820004.4.2)

This "fixity" is important to know who is applied to whom. It contains this information:

  • right associativity (of $): f $ g $ h $ x means f $ (g $ (h $ x)) - if you draw the abstract syntax tree (AST) where $ nodes are binary, you get a list-like tree, hanging to the right
  • left associativity (of #): a # b # c # d means ((a # b) # c) # d - tree hanging to the left
  • precedence (that of # is higher than that of $): f $ a # b means f $ (a # b).

Once we have the AST, we can insert type information at each node.

  • The left child of $ must be a function (of type t1 -> t2, for some types t1, t2), the right child must be of type t1, and the root (where $ is) has type t2. Often, t1 and t2 are patterns. The dynamic semantics of f $ x is "apply f to x"
  • For #, both children must be patterns, with a Unionable event type, and the root has the same pattern type. The semantics of a # b is "combine events of a and b"

The relation between the types you mention is

:i ControlPattern
type ControlPattern :: *
type ControlPattern = Pattern ControlMap
:i ControlMap
type ControlMap :: *
type ControlMap = Data.Map.Internal.Map String Value
:i Value
type Value :: *
data Value
  = VS {svalue :: String, vbus :: Maybe Int}
 | ...

A ControlPattern is what you can send to the back-end (with d1, etc.), but patterns with other event types might be used in construction.


About "extend the already awesome resources that exist with ..."

If someone wants to use the above, go ahead. But -

I'm afraid there's no short-cut here to the classical approach in programming languages:

  • concrete syntax (ASCII representation)
  • abstract syxtax (tree representation)
  • static semantics (types)
  • dynamic semantics (execution, evaluation) <== that's where you hear the music

but you might say I'm "biased" as I am getting paid for researching and teaching these things.

Now music (also, graphics) can be used in introductions to programming: students can start to do something audible/visible/creative immediately, while learning aspects of programming, and of a concrete language, along the way. E.g., https://www.cs.yale.edu/homes/hudak/SOE/index.htm , http://www.euterpea.com/haskell-school-of-music/ Also, see this recent thread about teaching Haskell https://mail.haskell.org/pipermail/haskell-cafe/2020-December/133125.html

But Tidal's goal is not "learning Haskell", it's "making music". The (hypothetical) claim "you can make music, without worring about Haskell" is risky. It somehow works as long as you stay in the mini language, and have a fixed expression on the outside, but it breaks down as soon as you 1) want to know why it's working, or 2) leave that fixed shape.

It is also risky (by me) preaching the one true Haskell way here - one possible outcome being "we don't need static semantics anyway" and switch (music) programming to Python or Javascript - an error that many universities are currently proudly making... But the fact is that types (type constructors like Pattern, type classes like Applicative) are not just nice for software correctness - they are also essential for effective programming (writing, and maintaining, less code - because that code can be much more expressive). Recent example: Ply and chords

There are lots of instances of this in Tidal's source , but they rarely become visible in performances. Perfectly understandable - there's a difference between changing some number (hardware synthesizer: turning a knob) and re-wiring the innards (soldering iron).

1 Like

Thanks a lot for taking the time to read and answer. I will have to process this. I really have to make my way and understand concepts which probably are basic stuff for you (static semantics as only one of many examples). Nevertheless I feel like this is mental workout. I actually enjoy watching talks about functional programming aso. understanding only ... well ... maybe 10 or 20 percent (to be fair to myself: Sometimes I do understand more than that;) )? It is probably my way to slowly get acquainted with ways of thinking which are somehow alien to me.

So yes, I want to make music with Tidal/Haskell which is what I am already doing. I don't really care too much about the code in these moments and more about the aesthetic outcome. Main criteria: is this interesting?

And also yes: I am very keen on understanding why and how this works. And there are practical reasons for that: Mainly I am eager to gain some kind of freedom in using Tidal because then it becomes easier to invent when you not always hounded by syntax errors. (There are also more pure or theoretical reasons which have to do with the mentioned 'mental workout'.)

The good thing: I am not in a hurry. So I when I have time and energy I will crawl down the rabbit whole and be glad if I can ask more or hopefully less silly questions here and get some answers also from time to time.

So again, I need some time to try and understand what you have written. Thanks!

ControlPattern is defined like this:

type ControlPattern = Pattern ControlMap

So it's a pattern of controlmaps, in the same way as a Pattern Int is a pattern of integer numbers.

What's a controlmap? Actually just a dictionary (aka associative array, hash map, lookup table etc), of keys and values, to represent a message to send to superdirt. Each key is the name of a parameter, like 'speed', with a value, like 1.5.

You say every doesn't seem to expect a ControlPattern:

every :: Pattern Int -> (Pattern a -> Pattern a) -> Pattern a -> Pattern a

The reason it works is you can replace the as with any other type, including ControlMap.. and ControlPattern is really just an alias for Pattern ControlMap.

The same for rev, it works on Pattern a, where a is unconstrained.. So it will work on ControlPatterns too.

speed :: Pattern Double -> ControlPattern

Is maybe clearer like this:

speed :: Pattern Double -> Pattern ControlMap

In that speed takes all the numbers in the pattern (with are of type Double) and replaces each one with a dictionary, each of which has only one entry, for speed, with the number as the value.

I don't understand what Carsten Heisterkamp is getting at with rule #4. I think it is not really true. In terms of syntax there are not really any special rules for using ControlMap or #, just very general Haskell rules for how to use operators, functions and parenthesis.

Looking at an example:

jux (# speed 2) $ sound "bd sn"

jux wants a function for its first parameter, which gets applied to the ControlPattern (aka Pattern ControlMap) in its second parameter. It could be something like rev of type Pattern a -> Pattern a, because 'a' can stand for a controlmap - rev doesn't care what kind of pattern its working on.

This wouldn't work, because speed 2 doesn't have the right type signature - it isn't even a function.

jux (speed 2) $ sound "bd sn"

# is a function, but it's one that takes two patterns as arguments. This working version works because it uses #, but with one of its arguments 'filled in'.. Which turns it into a new function that accepts the remaining argument.

jux (# speed 2) $ sound "bd sn"

In case it surprises you - # is a function, just like every. The only difference between operators like # and functions like # is that operators always have two arguments, and the first of its arguments appears on its left.

Hope that helps, please keep asking questions until it's clear!

2 Likes

Thanks a lot for this detailed response, very helpful indeed! I need some time to process as work is overwhelming right now.

This is especially insightful for me.

To be clear speed is a function, but speed 2 isn't, because it has all its inputs.

So if it is evaluated it is an applied function returning a value?

Yes that's right.

:t speed
speed :: Pattern Double -> ControlPattern
:t speed 2
speed 2 :: ControlPattern

Strictly speaking, a function is a special kind of value, that takes an input.

Thank you both, much appreciated advice. The fog is clearing. Slowly but surely.