“…done in Scheme.” was an idea I had when I started liking Scheme more and more. XMMS2 is my preferred audio player for a long time now. And I always wanted to write a client for it that fits my frame of mind better than the available ones.

I started writing one in C and that was okay. But when you're used to use properly extensible applications on a regular basis, you kind of want your audio player to have at least some of those abilities as well. I started adding a lua interpreter (which I didn't like), a Perl interpreter (which I did before in another application, but which is also not a lot of fun). So I threw it all away, setting out to write one in Scheme from scratch.

To interface the XMMS2 server, I was first trying to write a library that wrapped the C library that the XMMS2 project ships. But now I was back to writing C, which I didn't want to do. Someone on IRC in #xmms2 on freenode suggested to just implement the whole protocol in Scheme natively. I was a little intimidated by that, because the XMMS2 server supports a ton of IPC calls. But XMMS2 also ships with machine readable definitions of all of those, which means that you can generate most of the code and once you've implemented the networking part and have the serialization and de-serialization for the protocol's data types, you're pretty much set. …well, after you've implemented the code that generates your IPC code from XMMS2's definition file.

Most of XMMS2's protocol data types map very well to Scheme. There are strings, integers, floating point numbers, lists, dictionaries. All very natural in Scheme.

And then there are Collections. Collections are a way to interact with the XMMS2 server's media library. You can query the media library using collections. You can generate play lists using collections and perform a lot of operations on them like sorting them in a way you can specify yourself. For more information about collections see the Collections Concept page in the XMMS2 wiki.

Now internally, Collections are basically a tree structure, that may be nested arbitrarily. It carries a couple of payload data sets, but they are trees. And implementing a tree in Scheme is not all that hard either. The serialization and de-serialization is also pretty straight forward, since the protocol reuses its own data types to represent the collection data.

What is not quite so cool is the language you end up with to express these collections. Say, you want to create a collection that matches four Thrash Metal groups you can do that with XMMS2's command line client like so:

xmms2 coll create big-four artist:Slayer \
                        OR artist:Metallica \
                        OR artist:Anthrax \
                        OR artist:Megadeth

To create the same collection with my Scheme library, that would look like this:

(make-collection COLLECTION-TYPE-UNION
    '() '()
    (list (make-collection COLLECTION-TYPE-EQUALS
              '((field . "artist")
                (value . "Slayer"))
              '()
              (list (make-collection COLLECTION-TYPE-UNIVERSE '() '() '())))
          (make-collection COLLECTION-TYPE-EQUALS
              '((field . "artist")
                (value . "Metallica"))
              '()
              (list (make-collection COLLECTION-TYPE-UNIVERSE '() '() '())))
          (make-collection COLLECTION-TYPE-EQUALS
              '((field . "artist")
                (value . "Anthrax"))
              '()
              (list (make-collection COLLECTION-TYPE-UNIVERSE '() '() '())))
          (make-collection COLLECTION-TYPE-EQUALS
              '((field . "artist")
                (value . "Megadeth"))
              '()
              (list (make-collection COLLECTION-TYPE-UNIVERSE '() '() '())))))

…and isn't that just awful? Yes, yes it is. It so is.

In order to reign in this craziness, the library ships a macro that implements a little domain specific language to express collections with. Using that, the above boils down to this:

(collection (∪ (artist = Slayer)
               (artist = Metallica)
               (artist = Anthrax)
               (artist = Megadeth)))

So much better, right? …well, unless you really don't like Unicode characters and the ‘∪’ in there gives you a constant headache… But worry not, you can also use ‘or’ in place if the union symbol if you like. Or ‘UNION’ if you really want to be clear about things.

If you know a bit of Scheme, you may wonder how to use arguments to those operators that get evaluated at run time. Since evidently, the Slayer in there is turned into a "Slayer" string at compile time; same goes for the artist symbol. These transformations are done to make the language very compact for users to just type expressions. If you want an operator to be evaluated, you have to wrap it in a (| ...) expression:

(let ((key "artist") (value "Slayer"))
  (collection ((| key) = (| value)))

These expressions may be arbitrarily complex.

Finally, to traverse Collection tree data structures, the library ships a function called ‘collection-fold’. It implements pre, post and level tree traversal with both left-to-right and right-to-left directions. So, if you'd like to count the number of nodes in a collection tree, you can do it like this:

(define *big-four* (collection (∪ (artist = Slayer)
                                  (artist = Metallica)
                                  (artist = Anthrax)
                                  (artist = Megadeth)))

(collection-fold + 0 *big-four*)   ;; → 9

This would evaluate to 9.

The library is still at an early stage, but it can control an XMMS2 server as the ‘cli’ example, shipped with the library, will show you. There are no high level primitives to support synchronous and asynchronous server interactions. And there is not a whole lot of documentation, yet either. But the library implements all data types the protocol supports as well as all methods, signals and broadcasts the IPC document defines. The collection DSL supports all collection operators and all attributes one might want to supply.

Feel free to take a look, play around, report bugs. The library's home is with my account on github.

[Update] In a previous version of this post I mixed up the terminology for unions and intersections. The library mixed up the set operators associated with ‘and’ and ‘or’, too, and was fixed as well.

Posted Sun 15 Jan 2017 15:52:39 CET Tags: