I reinvented the wheel last week, and here’s why
Published:
Common wisdom tells you that many problems worth solving have already been solved for you (probably more than once), and that you should leverage this fact. In other words: don’t reinvent the wheel; stand on the shoulders of giants. I very often follow this rule of thumb. But last week, I broke that rule and wrote an F# implementation of the OSC (Open Sound Control) protocol1.
But first, what is OSC? Here’s a quick and incomplete overview (there’s plenty of information elsewhere on the intertubes): OSC is a simple but very flexible message format, used to allow devices or applications to talk to each other in ad-hoc and arbitrary ways. An OSC message consists of an address string, such as "/oscillators/1/frequency"
, and a list of arguments (which can be things like strings, integers, floats, etc). An OSC client sends messages to a server, which will attempt to dispatch the message based on the functionality it decides to expose. OSC also has pattern matching, which allows, for example, "/oscillators/*/frequency"
to simultaneously dispatch to "/oscillators/1/frequency"
, "oscillators/2/frequency"
, "oscillators/foobar/frequency"
, etc. It is quite powerful and flexible, and lots of things speak OSC. The spec defines simple encodings for these messages2. It was originally intended for use in music- and audio-based applications (such as synthesizers) as a potential alternative to MIDI, but has enjoyed wider multimedia applicability in things like lighting, animatronics, and robotics (to name a few).
Q2Q support for OSC (both sending a receiving) is the latest feature I’ve been working on, and it’s been a rabbit hole, but boy, has it been a fun rabbit hole. Since Q2Q is written in F#, I was looking for ways to send/receive OSC messages in the language. Though F# has a strong open-source community, it’s not too surprising that nobody has published a first-class OSC library. There are C# OSC libraries (such as SharpOsc), but I didn’t consider them an option because I knew I really wanted all the goodies that well-designed F#-oriented code awards you, such as immutability, making illegal states unrepresentable, and railway-oriented programming (ROP), algebraic data types—the list goes on. So I decided to reinvent the wheel and build an OSC library in F#. Besides, this was a wheel I wanted to learn about.
The specs for OSC 1.0 and OSC 1.1 are pretty short as far as specs go; only a couple pages each—you could read them over lunch. This makes it pretty straightforward to just drive out the whole implementation using TDD (Test Drive Development). The entire OSC 1.1 AST (excluding bundles and timetags, which I just didn’t get around to) consists of the following:
type OscAtom =
| OscInt32 of intValue:int32
| OscFloat32 of floatValue:float32
| OscString of stringValue:string
| OscBlob of blobData:byte[]
| OscBool of bool
| OscNone
| OscImpulse
type OscMessage = { addressPattern: string; arguments: OscAtom list }
Boom, Domain Designed.
Next step: Implementing each of the encoding/decoding functions for the atoms and messages, according to the spec. After that: the message dispatching / matching logic (which F# is particularly good at). Then, finally: the code that fires these messages over the wire (I wrote both UDP and TCP clients), as well as the code that can listen for OSC messages coming in from the network (UDP only for now). The whole thing is just shy of 600 lines of code. Sure, I could have spent that time doing other Q2Q features. But in this case, the advantages outweigh the time spent: this implementation will fit right in with the rest of Q2Q’s idiomatic F# codebase, and I know for a fact that it is correct to the spec3 since it has complete test coverage. There’s almost 2x the lines of test code than actual code. Sometimes that could be a code smell, but I would argue that it’s not when you’re implementing a protocol from its spec.
Do I always recommend reinventing the wheel? Definitely not. Would I recommend trying it from time to time? Absolutely.
Coming soon to theaters near you: OSC support in Q2Q 0.5.0! In the mean time, for geeks like me, you can browse the source code, or use the NuGet package.
-
Um, ackshually, it’s a content format, not a protocol… Yes, I know, dear reader, and they spell this out in the OSC 1.1 spec. I called it that for sake of simplicity. Calling it a “content format” may be more correct, but I think “protocol” gives a better connotation in that opening paragraph. You may notice that I more correctly use the “content format” description later on. Whatever, go ahead and crucify me for that if you’re feeling particularly pedantic today. I’ll still love you. You—yes, you, beloved reader. ↩
-
There’s also bundles, which I don’t get into this post. You can read more about those elsewhere. ↩
-
Well, as long as the messages coming in are well-formed. It doesn’t have particularly good handling of error cases for now, but I can always circle back later. The client also doesn’t do a particularly good job of sanitizing the messages before they’re sent (i.e. preventing you from using an address without a slash at the beginning). ↩