How to apply functions to number Patterns?

Greetings,

What kinds of ways are there to construct or apply number patterns from functions?

I was trying to import some custom arpeggiation code into Tidal. A minor victory was getting an old function to compile/work in Haskell:

wrap :: Int -> [Int] -> Int -> Int
wrap x seq jump
  | s < 0 = seq!!(s+len) + (j-1)*jump
  | otherwise = seq!!s + j*jump
  where len = length seq
        j = x `div` len
        s = x `mod` len

By reading Pattern.hs, I found that withValue could be used to apply a number function with a single argument to a number pattern.

withValue (\a -> wrap a [0,2,4] 7) "0 [2 4] [3 3 3]"

giving the expected result:

t> (0>⅓)|0
  (⅓>½)|4
  (½>⅔)|9
 (⅔>⁷₉)|7
(⁷₉>⁸₉)|7
 (⁸₉>1)|7

Now, withValue just works for a function with a single argument and a single pattern input.

What if I had multiple inputs to the function that I wanted to feed with number patterns? (My goal function has the type signature: Int -> Int -> [Int] -> Float -> Int -> Int... thus I would like to map at least two input Int patterns and possibly an array pattern.) What kind of strategies are there for constructing number patterns from complicated functions?

best regards

So, I think I am nearly there. In Params.hs I found pI and the like used to generate control patterns:

cp = pI "a" "0 2 3 4" # pI "b" "0 3 5"

resulting in

t> (0>¼)|a: 0, b: 0
(¼>⅓)-½|a: 2, b: 0
¼-(⅓>½)|a: 2, b: 3
(½>⅔)-¾|a: 3, b: 3
½-(⅔>¾)|a: 3, b: 5
  (¾>1)|a: 4, b: 5

Then, as an exercise, I tried adding them together directly, which did not work due to the polymorphic type of Value, but then found the fNum2 helper, which did:

--addNumbers vm = (vm Map.! "a") + (vm Map.! "b")
addNumbers vm = fNum2 (+) (+) (vm Map.! "a") (vm Map.! "b")

giving the expected result:

t> (0>¼)|0
(¼>⅓)-½|2
¼-(⅓>½)|5
(½>⅔)-¾|6
½-(⅔>¾)|8
  (¾>1)|9

It seems like my final adapter function needs an additional layer for pattern matching the possible types of Value inputs that come out of the ValueMap events from the ControlPattern.

I finally got it to work. My adapter function which gets passed to withValue looks like this:

carpVM :: [Int] -> ValueMap -> Int
carpVM seq vm = carp a b seq drag jump
  where mA = Map.lookup "a" vm
        mB = Map.lookup "b" vm
        mDrag = Map.lookup "drag" vm
        mJump = Map.lookup "jump" vm
        a = case mA of
          Just (VI v) -> v
          Nothing -> 0
        b = case mB of
          Just (VI v) -> v
          Nothing -> 0
        drag = case mDrag of
          Just (VF v) -> v
          Nothing -> 0
        jump = case mJump of
          Just (VI v) -> v
          Nothing -> 7

Pattern is instance Applicative so for a function with the signature

f :: Int -> Int -> Int

you can apply it to patterns like

f <$> "0 1 2 3" <*> "0 9 1"

If you want to use structure from only one side (like *|, |+ and the like), you can use *> and <* to use the structure only from right or left respectively. Incidentally these standard binary operators we all use every day in Tidal are defined exactly like that, e.g.,

a |* b = (*) <$> a <* b

Note: if you want to use *> and <*, you need to run import Prelude hiding ((<*), (*>)) to avoid name clashes with the Prelude module.

You can extend this to any number of flat arguments by chaining <*>, i.e., for g of type Int -> ... -> Int -> Int you write

g <$> a1 <*> ... <*> an

Now lifting functions with more structured signature, like your wrap function, requires a bit more of a mental stretch and will probably involve something like map, foldl, foldr and the like to get the types match up and may depend on the specific application. I'll leave the implementation details up to you for now but let me know if you are stuck and we can work it out together.

You can also do something like

(flip wrap [0, 2, 4]) <$> "0 [2 4] [3 3 3]" <*> "7 1"

which would allow you to use patterns for x and jump but not for seq.

Finally, it might be easier to rewrite the wrap function to work on Patterns directly rather than lifting if the only purpose is to use it in Tidal.

Good luck and happy experimenting!

2 Likes

Thank you for your illuminating suggestions, Frederik!

you can apply it to patterns like f <$> "0 1 2 3" <*> "0 9 1"

Indeed, I was able to call a modified variant of my original function like this:

( (carp2 [0,2,4]) <$> "[0 1 2 3 4]/3" <*> 0 <*> 1 <*> 7)

One downside is that this form forces us to use a lot of default parameters... that is, I would love to omit them (the scalars at the end) when I am not using them.

About the seq parameter, I have some fundamental doubts that you allude to:

Now lifting functions with more structured signature, like your wrap function, requires a bit more of a mental stretch and will probably involve something like map, foldl, foldr and the like to get the types match up and may depend on the specific application.

Is it possible to use the mini-notation superposition and somehow get Lists, arrays of values out of that? For example, with a pattern like "0 [2,4]", the superposed values are separated into coincident events:

t> (0>½)|0
(½>1)|2
(½>1)|4

If we used fmap or <$> it would probably process the events separately. We would probably want to find some way to collect the superposed events into a pattern of Lists. Maybe possible with one of the filter functions from Patterns.hs, perhaps?

If we used fmap or <$> it would probably process the events separately. We would probably want to find some way to collect the superposed events into a pattern of Lists. Maybe possible with one of the filter functions from Patterns.hs, perhaps?

As far as I know mini notation does not support lists but I'm not too familiar with it so I might be wrong. Your observation is correct that it would process them independently. To make things worse, you would get type errors, because you have scalars not lists. So as you suggested it would be nice to have a function listify :: Pattern a -> Pattern [a] that lumps simultaneous events together. It will not be easy if you are not familiar with Haskell and how patterns are implemented in Tidal but could be a good exercise in understanding it. (But I would really say it's an advanced exercise.)

The easier approach would be to not use mini notation but the functions fastcat, slowcat, fast, slow, etc. directly, e.g.,

fastcat [return [0], return [2,4]]

will create the example pattern. You can use the table from the documentation on mini-notation as a reference. If you find writing return for each element in the list tedious, you can map return over the whole list like so:

fastcat $ return <$> [[0], [2,4]]

or even define a shortcut like rt = (return <$>) if you're going to use a lot of nested structures which would shorten it to

fastcat $ rt [[0], [2,4]

Still not as concise as mini-notation but maybe enough for experimenting...

1 Like

Not having default parameters is fundamentally integrated into Haskell. There are some ways to have defaults using record types but it will not make things easier in your case. What people usually do in Haskell is define a general function and then a special case function with the default values set, e.g.,

degrade = degradeBy 0.5

Here degrade is defined as the special case of the degradeBy function, where the dropout probability is 0.5.

1 Like

I am definitely curious to go down this road, just a little bit. At the moment I am hard pressed to find where the Event type is defined (in order to understand Pattern).

I’m away from a computer at the moment but running :i Event in a tidal repl should tell you in which module it’s defined

Thanks, Paul. That was helpful for finding the definition!

1 Like

I see you keep rejecting my easy solutions so no more light treatment for you then... :sweat_smile:

How is your progress with the listify function? Do you need pointers? I got intrigued by the problem as well and implemented it myself using foldr and a few helper functions. If you need a clue how to get started you can reveal the spoiler below

Spoiler
listify :: Pattern a -> Pattern [a]
listify p = Pattern $ collect . query p
  where
    collect :: [Event a] -> [Event [a]]
    collect events = foldr add [] events
    add :: Event a -> [Event [a]] -> [Event [a]]
    add e es = undefined

The add function should check if the event e has an event in es that occurs at the same time and then add its value to the event. Otherwise it should add the event e to the list es (with the structure of its value adapted to the require type). You might want to use recursion. Let me know if you need more hints.

If you're going to get serious about writing code in Haskell, I strongly recommend installing the Haskell Language Server (hls) which makes "getting stuff to compile" and finding the definitions much easier. If you're using neovim I can help you set it up but for many other ide you will also find good documentation online.

How is your progress with the listify function?

I'm still working on it, after pausing to read a bit about Haskell types. Hopefully, I can try some experiments before looking at your solution.

Your construction of list patterns blew my mind:

fastcat [return [0], return [2,4]]

i.e. I had to verify that return [0] was actually a Pattern [Int]. Just now I was able to conclude that this works due to the Pattern implementation of Monad and Applicative, where pure and return, synonyms, construct a kind of constant pattern.

So, I have the first version based on your helpful template! I haven't checked all aspects for correctness, but it runs on a simple example. If it is not incorrect, then there are probably many ways to improve the style.

import Data.Maybe (maybe, isJust, fromJust)
import Data.List (find, findIndex)

listify :: Pattern a -> Pattern [a]
listify p = Pattern $ collect . query p
  where
    collect :: [Event a] -> [Event [a]]
    collect events = foldr add [] events

    --check if the event e has an event in es that occurs at the same time
    --and then add its value to the event.
    --Otherwise it should add the event e to the list es
    add :: Event a -> [Event [a]] -> [Event [a]]
    add e es
      | isJust matchedInd = replaceEventI es (fromJust matchedInd) augmentedE
      | otherwise = (convertEvent e):es
      where matchedE = find (eventsCoincident e) es
            matchedInd = findIndex (eventsCoincident e) es
            augmentedE = maybe (convertEvent e) (augmentEvent (value e)) matchedE

    --give an event augmented with a value
    augmentEvent :: a -> Event [a] -> Event [a]
    augmentEvent val ev = Event { context=context ev,
                                  whole=whole ev,
                                  part=part ev,
                                  value=val:(value ev) }

    --convert to a list event
    convertEvent :: Event a -> Event [a]
    convertEvent ev = Event { context=context ev,
                              whole=whole ev,
                              part=part ev,
                              value=[value ev]}

    --do two events  reflect the same time?
    eventsCoincident :: Event a -> Event [a] -> Bool
    eventsCoincident x y =
      (part x == part y) && (whole x == whole y)

    --replace the Event at index I
    replaceEventI :: [Event [a]] -> Int -> Event[a] -> [Event [a]]
    replaceEventI es i b = (fst split) ++ (b:(tail (snd split)))
      where split = splitAt i es
1 Like

Good job!

A few comments to make your code more concise:

update record types

There is a simplified syntax for updating record types in which case you only need to specify the new values which would allow you to write something like

convertEvent ev = ev { value = [value ev] }

and similarly for augmentEvent.

pattern matching

Instead of isJust and fromJust you can just pattern match against the different cases:

add e es = case matchedInd of
    Just match -> replaceEventI es match augmentedE
    Nothing    -> (convertEvent e):es
  where ...

which admittedly is not that much shorter but it avoides the partial (=evil) function fromJust.

multiple iterations

Now one thing that strikes me is that in the code as it is, you iterate over the same list of events multiple times, 1st to find the matching event, 2nd to find the matching events index and finally to split at that index and insert the new value. If you write the iteration yourself as a recursion, you can do all these things at once, i.e.,

add e [] = convertEvent e  -- base case (we have reached the end of the list)
add e e':es | eventsCoincident e e' = (augmentEvent (value e) e') : es  -- it's a match -> add value
add e e':es | otherwise             = e' : (add e es)                   -- no match -> continue recursion

conclusion

I see you make good use of the common types and functions in Haskell. If you haven't heard of it yet, I recomend you have a look at Hoogle which allows searching Haskell libraries by name and even by type thus providing you with the right tool for the right job many times (it even searches the tidal library). However, sometimes a more hands-on approach can produce more concise code but requires more familiarity with the functional paradigms like recursion and folds, just like a for-loop sometimes is the best approach in an imperative language. Spotting this is of cause a matter of practice so I'm confident this will get more and more easy for you.

Neat code doesn't really write itself. Sometimes when problems are a bit more complicated, I write pseudo code as a for loop and then translate the whole thing into functional style (sometimes when writing Python I go the other way around as well). This takes several steps from a rather bulky direct translation through several steps of finding patterns that allow for simplification until you end up with just a few lines of code -- just the way this discussion is going right now.