Making the Dream a Reality
Okay, enough fantasizing about good-looking code; now you need to get to work writing define-binary-class
--writing the code that will turn that concise expression of what an ID3 tag looks like into code that can represent one in memory, read one off disk, and write it back out.
To start with, you should define a package for this library. Here’s the package file that comes with the version you can download from the book’s Web site:
(in-package :cl-user)
(defpackage :com.gigamonkeys.binary-data
(:use :common-lisp :com.gigamonkeys.macro-utilities)
(:export :define-binary-class
:define-tagged-binary-class
:define-binary-type
:read-value
:write-value
:*in-progress-objects*
:parent-of-type
:current-binary-object
:+null+))
The COM.GIGAMONKEYS.MACRO-UTILITIES
package contains the with-gensyms
and once-only
macros from Chapter 8.
Since you already have a handwritten version of the code you want to generate, it shouldn’t be too hard to write such a macro. Just take it in small pieces, starting with a version of define-binary-class
that generates just the **DEFCLASS**
form.
If you look back at the define-binary-class
form, you’ll see that it takes two arguments, the name id3-tag
and a list of slot specifiers, each of which is itself a two-item list. From those pieces you need to build the appropriate **DEFCLASS**
form. Clearly, the biggest difference between the define-binary-class
form and a proper **DEFCLASS**
form is in the slot specifiers. A single slot specifier from define-binary-class
looks something like this:
(major-version u1)
But that’s not a legal slot specifier for a **DEFCLASS**
. Instead, you need something like this:
(major-version :initarg :major-version :accessor major-version)
Easy enough. First define a simple function to translate a symbol to the corresponding keyword symbol.
(defun as-keyword (sym) (intern (string sym) :keyword))
Now define a function that takes a define-binary-class
slot specifier and returns a **DEFCLASS**
slot specifier.
(defun slot->defclass-slot (spec)
(let ((name (first spec)))
`(,name :initarg ,(as-keyword name) :accessor ,name)))
You can test this function at the REPL after switching to your new package with a call to **IN-PACKAGE**
.
BINARY-DATA> (slot->defclass-slot '(major-version u1))
(MAJOR-VERSION :INITARG :MAJOR-VERSION :ACCESSOR MAJOR-VERSION)
Looks good. Now the first version of define-binary-class
is trivial.
(defmacro define-binary-class (name slots)
`(defclass ,name ()
,(mapcar #'slot->defclass-slot slots)))
This is simple template-style macro—define-binary-class
generates a **DEFCLASS**
form by interpolating the name of the class and a list of slot specifiers constructed by applying slot->defclass-slot
to each element of the list of slots specifiers from the define-binary-class
form.
To see exactly what code this macro generates, you can evaluate this expression at the REPL.
(macroexpand-1 '(define-binary-class id3-tag
((identifier (iso-8859-1-string :length 3))
(major-version u1)
(revision u1)
(flags u1)
(size id3-tag-size)
(frames (id3-frames :tag-size size)))))
The result, slightly reformatted here for better readability, should look familiar since it’s exactly the class definition you wrote by hand earlier:
(defclass id3-tag ()
((identifier :initarg :identifier :accessor identifier)
(major-version :initarg :major-version :accessor major-version)
(revision :initarg :revision :accessor revision)
(flags :initarg :flags :accessor flags)
(size :initarg :size :accessor size)
(frames :initarg :frames :accessor frames)))