Records

Classes in object-oriented programs tend to fall into two distinct categories: those that represent programming artifacts, such as String, Socket, InputStream, and OutputStream, and those that represent application domain information, such as Employee and PurchaseOrder.

Unfortunately, using classes to model application domain information hides it behind a class-specific micro-language of setters and getters. You can no longer take a generic approach to information processing, and you end up with a proliferation of unnecessary specificity and reduced reusability. See Clojure’s documentation on datatypes[37] for more information.

For this reason, Clojure has always encouraged the use of maps for modeling such information, and that holds true even with datatypes, which is where records come in. A record is a datatype, like those created with deftype, that also implements PersistentMap and therefore can be used like any other map (mostly); and since records are also proper classes, they support type-based polymorphism through protocols. With records, we have the best of both worlds: maps that can implement protocols.

What could be more natural than using records to play music? So, let’s create a record that represents a musical note, with fields for pitch, octave, and duration; then we’ll use the JDK’s built-in MIDI synthesizer to play sequences of these notes.

Since records are maps, we’ll be able to change the properties of individual notes using the assoc and update-in functions, and we can create or transform entire sequences of notes using map and reduce. This gives us access to the entirety of Clojure’s collection API.

We’ll create a Note record with the defrecord macro, which behaves like deftype.

 (defrecord name [& fields] & opts+specs)

A Note record has three fields: pitch, octave, and duration.

 (defrecord Note [pitch octave duration])
 -> user.Note

The pitch will be represented by a keyword like :C, :C#, and :Db, which represent the notes C, C (C sharp), and D (D flat), respectively. Each pitch can be played at different octaves; for instance, middle C is in the fourth octave. Duration indicates the note length; a whole note is represented by 1, a half note by 1/2, a quarter note by 1/4, and a 16th note by 1/16. For example, we can represent a D half note in the fourth octave with this Note record:

 (->Note :D# 4 1/2)
 -> #user.Note{:pitch :D#, :octave 4, :duration 1/2}

We can treat records like any other datatype, accessing their fields with the dot syntax.

 (.pitch (->Note :D# 4 1/2))
 -> :D#

But records are also map-like:

 (map? (->Note :D# 4 1/2))
 -> true

so we can also access their fields using keywords:

 (:pitch (->Note :D# 4 1/2))
 -> :D#

We can create modified records with assoc and update-in.

 (assoc (->Note :D# 4 1/2) :pitch :Db :duration 1/4)
 -> #user.Note{:pitch :Db, :octave 4, :duration 1/4}
 
 (update-in (->Note :D# 4 1/2) [:octave] inc)
 -> #user.Note{:pitch :D#, :octave 5, :duration 1/2}

Records are open, so we can associate extra fields into a record:

 (assoc (->Note :D# 4 1/2) :velocity 100)
 -> #user.Note{:pitch :D#, :octave 4, :duration 1/2, :velocity 100}

Use the optional :velocity field to represent the force with which a note is played.

When used on a record, both assoc and update-in return a new record, but the dissoc function works a bit differently; it will return a new record if the field being dissociated is optional, like velocity in the previous example, but it will return a plain map if the field is mandated by the defrecord specification, like pitch, octave, or duration.

In other words, if you remove a required field from a record of a given type, it’s no longer a record of that type, and it simply becomes a map.

 (dissoc (->Note :D# 4 1/2) :octave)
 -> {:pitch :D#, :duration 1/2}

Notice that dissoc returns a map, not a record. One difference between records and maps is that, unlike maps, records are not functions of keywords.

 ((->Note. :D# 4 1/2) :pitch)
 -> user.Note cannot be cast to clojure.lang.IFn

ClassCastException is thrown because records do not implement the IFn interface like maps do. This is by design and drives a stylistic difference that makes code more readable.

When accessing a collection, you should place the collection first. When accessing a map that’s acting (conceptually) as a data record, you should place the keyword first, even if the record is implemented as a plain map. Now that we have our basic Note record, let’s add some methods so we can play them with the JDK’s built-in MIDI synthesizer. We’ll start by creating a MidiNote protocol with three methods:

 (defprotocol MidiNote
  (to-msec [this tempo])
  (key-number [this])
  (play [this tempo midi-channel]))

To play our note with the MIDI synthesizer, we need to translate its pitch and octave into a MIDI key number and its duration into milliseconds. Here, we’ve defined to-msec, key-number, and play, which we will use to create our MidiNote.

  • to-msec returns the duration of the note in milliseconds.
  • key-number returns the MIDI key number corresponding to this note.
  • play plays this note at the given tempo on the given channel.

Now let’s extend our Note record to implement the MidiNote protocol.

 (import ​'javax.sound.midi.MidiSystem​)
 (extend-type Note
  MidiNote
  (to-msec [this tempo]
  (​let​ [duration-to-bpm {1 240, 1/2 120, 1/4 60, 1/8 30, 1/16 15}]
  (* 1000 (/ (duration-to-bpm (:duration this))
  tempo))))

The to-msec function translates the note’s duration from whole note, half note, quarter note, and so on, into milliseconds based on the given tempo, which is represented in beats per minute (bpm).

 (key-number [this]
  (​let​ [scale {:C 0, :C# 1, :Db 1, :D 2,
  :D# 3, :Eb 3, :E 4, :F 5,
  :F# 6, :Gb 6, :G 7, :G# 8,
  :Ab 8, :A 9, :A# 10, :Bb 10,
  :B 11}]
  (+ (* 12 (inc (:octave this)))
  (scale (:pitch this)))))

The key-number function maps the keywords used to represent pitch into a number ranging from 0 to 11 [1] and then uses this number along with the given octave to find the corresponding MIDI key-number.

 (play [this tempo midi-channel]
  (​let​ [velocity (or (:velocity this) 64)]
  (.noteOn midi-channel (key-number this) velocity)
  (Thread/sleep (to-msec this tempo)))))

Finally, the play method takes a note, a tempo, and a MIDI channel; sends a noteOn message to the channel; and then sleeps for the note’s duration. The note continues to play even while the current thread is asleep, stopping only when the next note is sent to the channel.

Now we need a function that sets up the MIDI synthesizer and plays a sequence of notes:

 (​defn​ perform [notes & {:keys [tempo] :or {tempo 120}}]
  (with-open [synth (doto (MidiSystem/getSynthesizer) .open)]
  (​let​ [channel (aget (.getChannels synth) 0)]
  (doseq [note notes]
  (play note tempo channel)))))

The perform function takes a sequence of notes and an optional tempo value, opens a MIDI synthesizer, gets a channel from it, and then calls each note’s play method.

All the pieces are in place, so let’s make music using a sequence of Note records:

 (​def​ close-encounters [(->Note :D 3 1/2)
  (->Note :E 3 1/2)
  (->Note :C 3 1/2)
  (->Note :C 2 1/2)
  (->Note :G 2 1/2)])
 -> #​'user/close-encounters

In this case, our “music” consists of the five notes used to greet the alien ships in the movie Close Encounters of the Third Kind. To play it, just pass the sequence to the perform function:

 (perform close-encounters)
 -> nil

We can also generate sequences of notes dynamically with the for macro.

 (​def​ jaws (​for​ [duration [1/2 1/2 1/4 1/4 1/8 1/8 1/8 1/8]
  pitch [:E :F]]
  (Note. pitch 2 duration)))
 -> #​'user/jaws
 
 (perform jaws)
 -> nil

The result is the shark theme from Jaws—a sequence of alternating E and F notes progressively speeding up as they move from half notes to quarter notes to eighth notes.

Since notes are records and records are map-like, we can manipulate them with any Clojure function that works on maps. For instance, we can map the update-in function across the Close Encounters sequence to raise or lower its octave.

 (perform (map #(update-in % [:octave] inc) close-encounters))
 -> nil
 
 (perform (map #(update-in % [:octave] dec) close-encounters))
 -> nil

Or we can create a sequence of notes that have progressively larger values of the optional :velocity field:

 (perform (​for​ [velocity [64 80 90 100 110 120]]
  (assoc (Note. :D 3 1/2) :velocity velocity)))
 -> nil

This results in a sequence of increasingly more forceful D notes. Manipulating sequences is a particular strength of Clojure, so there are endless possibilities for programmatically creating and manipulating sequences of Note records.

Let’s put the MidiNote protocol, the Note record, and the perform function together in a Clojure source file called src/examples/midi.clj so we can use them in the future.

 (ns examples.datatypes.midi
  (:import [javax.sound.midi MidiSystem]))
 (defprotocol MidiNote
  (to-msec [this tempo])
  (key-number [this])
  (play [this tempo midi-channel]))
 
 (​defn​ perform [notes & {:keys [tempo] :or {tempo 88}}]
  (with-open [synth (doto (MidiSystem/getSynthesizer).open)]
  (​let​ [channel (aget (.getChannels synth) 0)]
  (doseq [note notes]
  (play note tempo channel)))))
 
 (defrecord Note [pitch octave duration]
  MidiNote
  (to-msec [this tempo]
  (​let​ [duration-to-bpm {1 240, 1/2 120, 1/4 60, 1/8 30, 1/16 15}]
  (* 1000 (/ (duration-to-bpm (:duration this))
  tempo))))
  (key-number [this]
  (​let​ [scale {:C 0, :C# 1, :Db 1, :D 2,
  :D# 3, :Eb 3, :E 4, :F 5,
  :F# 6, :Gb 6, :G 7, :G# 8,
  :Ab 8, :A 9, :A# 10, :Bb 10,
  :B 11}]
  (+ (* 12 (inc (:octave this)))
  (scale (:pitch this)))))
  (play [this tempo midi-channel]
  (​let​ [velocity (or (:velocity this) 64)]
  (.noteOn midi-channel (key-number this) velocity)
  (Thread/sleep (to-msec this tempo)))))
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.195.34