Instance Monad Pattern should use squeezeJoin instead of unwrap?

Tidal has four functions of type Pattern (Pattern a) -> Pattern a. (squeeze|inner|outerJoin|unwrap).

Each such join can be used to define bind, in a Monad instance, and this gets used when we write do notation.

I find squeezeJoin most useful for defining global structure, and I'd like to write

d1 $ do
  b <- segment 2 $ irand 7
  stack
    [ s "supersaw" + nsm (b - "[14 [~ 10]]")
    , do
        e <- segment 2 $ irand 3
        d <- segment 1 $ irand 2
        s "superpwm"
          + nsm (b + (d + 1) * run (2^e) )
    ] 

but this does not work as expected since instance Monad Pattern uses unwrap.

Full code, for comparison: https://git.imn.htwk-leipzig.de/waldmann/computer-mu/-/tree/master/tidal/code/bind (beware, the snippet sounds terrible, but it shows the idea)

I am not really suggesting to make that change now - I think what I am asking is: what was the reason to define bind via unwrap?

Oh, and I should also be asking - do any of these four obey Monad laws (modulo some observational equivance)?

[EDIT] well one reason certainly is that bind-via-unwrap fits with <*>

let m1 = fastcat [ pure succ, pure pred ]
    m2 = fastcat @Integer [2, 4, 8]

m1 <*> m2  
   ==>  (0>⅓)|3  (⅓>½)|5  (½>⅔)|3  (⅔>1)|7
m1 >>= (\x1 -> m2 >>= (\x2 -> return (x1 x2)))
   ==> same as above, 

as it shold be, https://hackage.haskell.org/package/base-4.21.0.0/docs/Control-Applicative.html#t:Applicative, "if f is also a Monad",

while this gives something different

let    bind p f = squeezeJoin (fmap f p)

m1 `bind` (\x1 -> m2 `bind` (\x2 -> return (x1 x2)))
   ==> (0>⅙)|3 (⅙>⅓)|5 (⅓>½)|9 (½>⅔)|1 (⅔>⅚)|3 (⅚>1)|7

Still I wonder - what are known use cases of do notation for patterns?

3 Likes

i remember making an issue about possibly using the QualifiedDo extension, which would let you write something like Squeeze.do, Inner.do, but ofcourse usually you would like to have different binds inside of one do block (i guess), so this is still not good enough...
i also tried thinking about the monad laws once, but not totally sure if they are satisfied by all joins.. (and yes, as you noted each join would also have it's own Applicative instance to match it)

1 Like

Ah, nice, I didn't think of qualified-do.

"different binds inside one block": yes, then we'd need (hypothetical) qualified-back-arrow

do  x Squeeze.<- foo
    y <- Inner.<- bar
    ...  

or introduce an extra block

Squeeze.do x <- foo
     Inner.do y <- bar
              ...

GHC does allow a mixture of nested-list (standard) and zip-list (non-standard) list comprehensions https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/parallel_list_comprehensions.html .

ghci> [ (x,y,z) | x <- [1,2] , y <- [3,4 ] | z <- [5..]]
[(1,3,5),(1,4,6),(2,3,7),(2,4,8)]

(TIL, by looking up the documentation) we can use this notation for any type that instantiates Monad and MonadZip https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/monad_comprehensions.html

But - I can't get it work, cf. source https://git.imn.htwk-leipzig.de/waldmann/computer-mu/-/blob/master/tidal/code/bind/monadcomp.tidal and question https://gitlab.haskell.org/ghc/ghc/-/issues/25646

Disregarding syntax - I think I find it clearer semantically if Tidal would have
instance MonadZip Pattern where mzip p q = (,) <$> p <*> q (using unwrap), and Monad via squeezeJoin.

Ah nice topic!

I wrote unwrap out of necessity and only later realised it was related to monads. I still have not really understood monadic laws and so no surprises if tidal's joins don't fit those laws. Still, they are very useful!

I never thought about making squeezeJoin the default, simply because it's a relatively new addition to tidal. Actually I think there are a lot more possible joins than are currently implemented (including a polymetric join that makes use of the tactus).

So yes, I would absolutely love it if we could find a way to flexibly choose different binds/joins within the parameters of an tidal expression.

I would rename unwrap to mixJoin (as is the case in the javascript port), as it preserves the onsets of both inner and outer events. We should also define the binds to go with all the joins, which seem missing in tidal at the moment.

found one: https://hackage.haskell.org/package/tidal-1.9.5/docs/src/Sound.Tidal.UI.html#spreadChoose

Monad laws: I think some of the laws do hold, for some versions of (>>=), if you squint hard enough (using observable equivalence as defined in test/TestUtils)

It might be a nice project to find the truth here (e.g., using https://hackage.haskell.org/package/leancheck) and make a composition based on the laws that are broken ...

Monad laws: I wrote a program to check these, https://git.imn.htwk-leipzig.de/waldmann/computer-mu/-/tree/master/tidal/monad-laws?ref_type=heads

interpretation of results is up for debate - am I using the correct notion of equivalence?

Very interesting!

I would say that an [Event {whole = (0,1), part = (0,1), value = 'a'}] would be equivalent to two or more event fragments that make up that whole, e.g. [Event {whole = (0,1), part = (0,0.25), value = 'a'}, Event {whole = (0,1), part = (0.25,1), value = 'a'}].

Some of Tidal's tests merge fragments together before comparing expected events with actual results by calling defragParts on the event lists first. So maybe it helps to use the same function in your lawyer script.

I do use defragParts https://git.imn.htwk-leipzig.de/waldmann/computer-mu/-/blob/master/tidal/monad-laws/TML.hs?ref_type=heads#L43

Building the map (representing a multi-set of events) is really the same thing as sorting the list (in Tidal/test/TestUtils) so I could/should have just used that.

1 Like