Pink and Score

I’m conscious of relevance, so I’m not going to post anything else on this thread.

I’d be happy to talk about CL-Collider, or LISP for music/DSP/livecoding in a new topic, if @smoge, or anyone else, wants to start one.

@cian I’m actually doing very similar things but in Haskell, not CL.

For example, I can insert Rhythm Trees or lilypond syntax as part of the haskell code:

example :: Rtm
example = [rtm| (1 -1 (1 (1 -1 1))) |]

or

p = [pitch|cs' gf,|]

-- [C Sharp Octave 5,G Flat Octave 3]

If you want to continue this discussion, just reply with a new topic

1 Like

Yeah I’m not sure where the best place to put it would be, but I’d love to talk about micro-notation ‘stuff’.

Our colleague Rohan Drape did it a while ago. And if you’re designing the language, the sky is the limit; there’s no reason to think it is not ‘well suited’.

What I find super cool is when programming language designers discover very fundamental things while writing a new language. That was the case with the original MIT Scheme (well documented)

More recently, I saw at least two experimental language projects that independently concluded that LISP macros and Haskell’s Type Classes have a lot of things in common; one is a particular case of the other, and at the same time, nothing prohibits from becoming the same thing in new programming designs.
(For reference, see Alexis King’s Hackett https://www.youtube.com/watch?v=5QQdI3P7MdY
and Turnstile https://www.khoury.northeastern.edu/home/stchang/pubs/ckg-popl2017.pdf )

I personally think Racket is ill-suited for SuperCollider because:

  • it’s not dynamic
  • doesn’t have a REPL
  • has poor performance in ways that matter to me (MIDI)
  • doesn’t have great libraries

That doesn’t mean that I think it’s a bad language, or that learning it is a waste of time. It’s just that when I’m making music those things get in the way of ME making music. And ultimately I just want a tool for making music quickly.

But try it and see. Maybe I’m wrong. Wouldn’t be the first time.

What I find super cool is when programming language designers discover very fundamental things while writing a new language .

Sure. Academic research can result in cool outcomes.

If you’re talking about livecoding languages - then yes there are lots of interesting experiments out there. Where you would struggle trying to do this in Racket, is that livecoding languages should be dynamically updatable (e.g. you can update code while code is already running), and Racket’s not really setup for that. Haskell can kind of do this, though not brilliantly. Common Lisp does this ridiculously well. Other languages probably also have reasonable tools for this (I’d guess Julia, Ruby and Python, probably more).

Julia is insanely cool incidentally.

More recently, I saw at least two experimental language projects that independently concluded that LISP macros and Haskell’s Type Classes have a lot of things in common; one is a particular case of the other, and at the same time, nothing prohibits from becoming the same thing in new programming designs.

I think you want to read the first paper more carefully. Something all experienced Haskell programmers know is that you can create language constructs (up to and including major DSLs) using typeclasses (for example, this is where the infamous monad can be useful), and to a lesser degree laziness. But it can’t do all the things that macros can do, and you are at the mercy of the compiler for optimization (getting good performance in Haskell can be something of a black art).

Why typeclasses are cool in a language like Haskell, and people are constantly looking for ways to extend them, is that it allows you to design a language in a ‘principled’ way so that you know it is properly formed. You cannot do this easily with macros. On the other hand - macros allow you to do more things (because you can break the language if you want, and sometimes that’s actually fine), and you can optimize your code in ways that are impossible with typeclasses (e.g. the infamous loop operator in LISP) using things like treewalking, and various optimizations.

Racket is a language, like Haskell and Rust, where the designers are obsessed with building a completely safe and principled language, where incorrect code is theoretically impossible. LISP comes from a different tradition that values flexibility and power where the language assumes you know what you’re doing, that it should only get in your way if you ask it to, and gives you the tools to fix it if something goes wrong.

Neither approach is wrong if implemented correctly, but both come with their tradeoffs. I’d rather my plane’s code be implemented using a principled language, but on the other hand if I’m prototyping code, or hacking for fun (which my SuperCollider and graphics stuff is), then I’d rather use a flexible language.

Other people obviously have different values, and that’s totally fine. Haskell is clearly an excellent environment for making music (Rohan Drape has been using it for years, and obviously Tidal was built in it. There’s also a very neat CSound front end built in it), it just doesn’t suit the way I work.

What paper? Those are not about Haskell but the re-implementation of those ideas.

I’m writing an interpreter in Haskell. But I think there was some confusion in your comment. The DSL in Haskell can benefit tremendously from the rich type system exposed in the host language. Type classes are one of the most trivial type-level programming in Haskell. This can go much deeper than that if one wants that, but in this case, there is nothing special about it. (And LISP, as the host language, doesn’t offer any of that… the point of the paper is to keep this rich information and also keep the macros… sort of )

Any interpreter needs a Monad, and I don’t understand the relationship between those things. An interpreter needs the State Monads, at the very least. The parser is also monadic.

Type Class is a fundamental feature but doesn’t necessarily imply a monadic type constructor. It’s a particular class among others…

I also sometimes use Template Haskell. Maybe that was your confusion??? in this case, Template is a monad used for metaprogramming. The Q monad allows for compile-time metaprogramming.
Dec is a type of a declaration. Declarations are constructs of names with associated types and values, such as data type definitions, function signatures, and value definitions (value bindings). The type signature (SigD) declares that a variable (varName) has type Pitch. And so on. It’s very different from regular Haskell code, nobody likes it:

generatePitchVars :: [String] -> Q [Dec]
generatePitchVars pitchNames = concatForM pitchNames $ \name -> do
    let varName = mkName name
        pitchVal = AppE (VarE 'fromString) (LitE (StringL name))
    pure [SigD varName (ConT ''Pitch), ValD (VarP varName) (NormalB pitchVal) []]

OK, after defining this Template, we could put it to use in another module. This generates all possible pitches (all octaves, all microtones, etc); the list is just arbitrarily using some of them as an example:

$(generatePitchVars (Map.keys pitchMap))

pitches :: [Pitch]
pitches =
    [ c
    , cis
    , ces
    , cisis
    , ceses
    , d
    , dis
    , des
    , dih___
    , deh'''
    , d__
    , e
    , ees
    , eis
    , eeh
    , feseh
    , eih
    , f
    ]

Any interpreter needs a Monad, and I don’t understand the relationship between those things. An interpreter needs the State Monads, at the very least. The parser is also monadic.

None of these things are true. You can use monads for all of those things and they can be a nice abstraction. There was life before the monad. Somehow people wrote interpreters and parsers prior to Philip Wadler’s seminal paper of 1995.

I programmed in Haskell for 6 years and I’m very familiar with the intricacies of its type system. I did not realize that you were familiar with it, which is why I dumbed down my comment.

There’s a reason why I don’t use Haskell now. It’s cool that you like it and are excited by it. I am not. Good luck with your future endeavors. If you use Racket, let us know how that goes.

The funny thing about monads is that in so many cases, like parsers, the monadic type constructor is the least interesting thing about the program, but people tend to concentrate on that.

I prefer F# terminology (“workflows”), it’s way more familiar and doesn’t call more attention than necessary. Instead of “Maybe Monad”, they say “Option Workflow”. I like it.

Yea, one can do it, you’re right on that. But most implementations do it.

Hi Jordan, thaks for sharing the picture of your score - it looks pretty damn well! Could you help me to find the direction how to make such as graphic score in lilypond?

2 Likes

Another one thinking about similar things: Monad confusion and the blurry line between data and computation - Micah Cantor

The macro example he gives works very differently to the monadic one. The comparison works in so much as you can do (some) of the same things with monads, but macros are not a subset of monads any more than monads are a subset of macros.

I’ll add that the only reason you can do this easily in Haskell is because it’s lazy. There’s a great talk by Simon Peyon-Jones somewhere where he talks about this (along with some of the downsides of laziness).

Interesting. He was in the audience of the Alexis King talk I shared with you, and he asked a question: “What type is a macro?” It’s a computation from AST → AST? What does it mean? (BTW, you mentioned scheme continuations, Alexis had a proposal/pr accepted into GHC)
As Philip Wadler recently pointed out, even with its rich type system that can smoothly work with all kinds of effects and states, it doesn’t make it easy to apply this type of system " to the other side".

And AST is simply a data structure that represent the structure of a piece of code more efficiently than a string. It doesn’t have to be correct (and often it isn’t correct), and it’s the product of the first step of a compiler. LISPs are unique in that the AST is identical to the code (because the language is really just structured lists/trees - which is identical to an AST) *.

A macro is a piece of code that takes an AST and transforms it into another AST. There is no guarantee that the AST will be a correctly compiling piece of code, and unless you put significant limitations on the macro, there is no way to guarantee that (as it would violate the halting problem). Informally you could state that an AST is a pretty weak type. A monad is a far stronger type (you can guarantee that it will output some meaningful form of computation, even if you can’t necessarily guarantee what it will be), but this comes at the expense of limiting what you can do.

Engineering is about trade offs. Beyond some level of optimization you always have to trade one thing for another (security for speed, or whatever). Macros are more powerful than monads, but this comes at the cost of more potential bugs. Common LISP hackers mostly don’t care, Racket programmers care quite a bit about this (which is why they created a safer, but more limited macro system), Haskell programmers obviously care a hell of a lot about this.

(*) This just means that macros are very easy to work with in LISP, unlike other languages, because you can literally output code from a macro and it just works. This is impossible in other languages, such as Rust, that have powerful macro systems where you have to work with trees that are pretty far removed from the syntactical code that you write.