6.031
6.031 — Software Construction
Fall 2020

Reading 27: Little Languages I

Software in 6.031

Safe from bugsEasy to understandReady for change
Correct today and correct in the unknown future. Communicating clearly with future programmers, including future you. Designed to accommodate change without rewriting.

Objectives

In this reading we will begin to explore the design of a little language for constructing and manipulating music. Here’s the bottom line: when you need to solve a problem, instead of writing a program to solve just that one problem, build a language that can solve a range of related problems.

The goal for this reading is to introduce the idea of representing code as data and familiarize you with an initial version of the music language.

Representing code as data

Recall the Formula datatype from Recursive Data Types:

Formula = Variable(name:String)
          + Not(formula:Formula)
          + And(left:Formula, right:Formula)
          + Or(left:Formula, right:Formula)

We used instances of Formula to take propositional logic formulas, e.g. (p ∧ q), and represent them in a data structure, e.g.:

And(Variable("p"), Variable("q"))

In the parlance of grammars and parsers, formulas are a language, and Formula is an abstract syntax tree.

But why did we define a Formula type? Java already has a way to represent expressions of Boolean variables with logical and, or, and not. For example, given boolean variables p and q, we can just write:

p && q

Done!

The answer is that the Java code expression p && q is evaluated as soon as we encounter it in our running program. The Formula value And(...) is a first-class value that can be stored, passed and returned from one method to another, manipulated, and evaluated now or later (or more than once) as needed.

The Formula type is an example of representing code as data, and we’ve seen others. Here is a functional object:

class AndFunction implements BiFunction<Boolean,Boolean,Boolean> {
    public boolean apply(boolean p, boolean q) {
        return p && q;
    }
}

BiFunction<Boolean,Boolean,Boolean> represents a function that takes two boolean values and produces a boolean value. An instance of BiFunction is a value that can be passed around, returned, and stored. But at any time, the function that it represents can be invoked by calling its apply method:

BiFunction<Boolean,Boolean,Boolean> f = new AndFunction();
boolean p = true;
boolean q = false;
boolean result = f.apply(p, q);

Lambda expressions allow us to create functional objects with a compact syntax:

BiFunction<Boolean,Boolean,Boolean> f = (p, q) -> p && q;

Building languages to solve problems

When we define an abstract data type, we’re extending the universe of built-in types provided by Java to include a new type, with new operations, appropriate to our problem domain. This new type is like a new language: a new set of nouns (values) and verbs (operations) we can manipulate. Of course, those nouns and verbs are abstractions built on top of the existing nouns and verbs which were themselves already abstractions.

A language has greater flexibility than a mere program, because we can use a language to solve a large class of related problems, instead of just a single problem.

  • That’s the difference between writing p && q and devising a Formula type to represent the semantically-equivalent Boolean formula.

  • And it’s the difference between writing a matrix multiplication function and devising a MatrixExpression type to represent matrix multiplications — and store them, manipulate them, optimize them, evaluate them, and so on.

This kind of language is called a domain-specific language (DSL), because it solves problems in a narrower domain than a general-purpose programming language like Java or Python. DSLs can be further classifed into external and internal. External DSLs have custom syntax and semantics, independent of any general-purpose programming language. Examples that we’ve already seen in this class include regular expressions, ParserLib grammars, and the language of problem set 3. By contrast, an internal DSL is embedded in a general-purpose programming language, using the syntax and abstraction mechanisms of the host language rather than inventing its own. First-class functions and functional objects enable us to create particularly powerful internal DSLs because we can capture patterns of computation as reusable abstractions. The music language that we’ll develop in this class and the next is an example of an internal DSL.

Music language

In class, we will design and implement a language for generating and playing music. To prepare, let’s first understand the Java APIs for playing music with the MIDI synthesizer. We’ll see how to write a program to play MIDI music. Then we’ll begin to develop our music language by writing a recursive abstract data type for simple musical tunes. We’ll choose a notation for writing music as a string of characters, and we’ll implement a parser to create instances of our Music type.

See the full source code for the basic music language.

Download the ZIP file at that link, unpack it, and import it into Eclipse, so that you can run the code and follow the discussion below.

Playing MIDI music

music.midi.MidiSequencePlayer uses the Java MIDI APIs to play sequences of notes. It’s quite a bit of code, and you don’t need to understand how it works.

MidiSequencePlayer implements the music.SequencePlayer interface, allowing clients to use it without depending on the particular MIDI implementation. We do need to understand this interface and the types it depends on:

addNote : SequencePlayer × Instrument × Pitch × double × double → void (SequencePlayer.java) is the workhorse of our music player. Calling this method schedules a musical pitch to be played at some time during the piece of music.

play : SequencePlayer → void (SequencePlayer.java) actually plays the music. Until we call this method, we’re just scheduling music that will, eventually, be played.

The addNote operation depends on two more types:

Instrument is an enumeration of all the available MIDI instruments.

Pitch is an abstract data type for musical pitches (think keys on the piano keyboard).

Read and understand the Pitch documentation and the specifications for its public constructor and all its public methods.

Our music data type will rely on Pitch in its rep, so be sure to understand the Pitch spec as well as its rep and abstraction function.

Using the MIDI sequence player and Pitch, we’re ready to write code for our first bit of music!

Read and understand the music.examples.ScaleSequence code.

Run the main method in ScaleSequence. You should hear a one-octave scale!

reading exercises

transpose

Pitch.transpose(int) is a:

(missing explanation)

Pitch

Which observers could MidiSequencePlayer feasibly use to determine what frequency an arbitrary Pitch represents?

(missing explanation)

addNote

SequencePlayer.addNote(..) is a:

(missing explanation)

Music data type

The Pitch datatype is useful, but if we want to represent a whole piece of music using Pitch objects, we should create an abstract data type to encapsulate that representation.

To start, we’ll define the Music type with a few operations:

notes : String × Instrument → Music (MusicLanguage.java) makes a new Music from a string of simplified abc notation, described below.

duration : Music → double (Music.java) returns the duration, in beats, of the piece of music.

play : Music × SequencePlayer × double → void (Music.java) plays the piece of music using the given sequence player.

We’ll implement duration and play as instance methods of Music, so we declare them in the Music interface.

notes will be a static factory method; rather than put it in Music (which we could do), we’ll put it in a separate class: MusicLanguage will be our place for all the static methods we write to operate on Music.

Now that we’ve chosen some operations in the spec of Music, let’s choose a representation.

  • Looking at ScaleSequence, the first concrete variant that might jump out at us is one to capture the information in each call to addNote: a particular pitch on a particular instrument played for some amount of time. We’ll call this a Note.

  • The other basic element of music is the silence between notes: Rest.

  • Finally, we need a way to glue these basic elements together into larger pieces of music. We’ll choose a tree-like structure: Concat(m1,m2:Music) represents m1 followed by m2, where m1 and m2 are any music.

    This tree structure turns out to be an elegant decision as we further develop our Music type later on. In a real design process, we might have had to iterate on the recursive structure of Music before we found the best implementation.

Here’s the datatype definition:

Music = Note(duration:double, pitch:Pitch, instrument:Instrument)
        + Rest(duration:double)
        + Concat(m1:Music, m2:Music)

Composite

Music is an example of the composite pattern, in which single objects (primitives, e.g. Note and Rest) and groups of objects (composites, e.g. Concat) all belong to the same type, so they can be treated the same way.

  • The recursive data types we’ve been using, like Formula, have been examples of the composite pattern.

  • The graphical user interface (GUI) view tree relies heavily on the composite pattern: there are primitive views like JLabel and JTextField that don’t have children, and composite views like JPanel and JScrollPane that do contain other views as children. Both implement the common JComponent interface.

The composite pattern gives rise to a tree data structure, with primitives at the leaves and composites at the internal nodes.

Emptiness

One last design consideration: how do we represent the empty music? It’s always good to have a representation for nothing, and we’re certainly not going to use null.

We could introduce an Empty variant, but instead we’ll use a Rest of duration 0 to represent emptiness.

Implementing basic operations

First we need to create the Note, Rest, and Concat variants. All three are straightforward to implement, starting with constructors, checkRep, some observers, toString, and the equality methods.

  • Since the duration operation is an instance method, each variant implements duration appropriately.

  • The play operation is also an instance method; we’ll discuss it below under implementing the player.

We’ll discuss the notes operation, implemented as a static method in class MusicLanguage under implementing the parser.

Read and understand the Note, Rest, and Concat classes.

To avoid representation dependence, let’s add some additional static factory methods for building Music instances:

note : double × Pitch × Instrument → Music (MusicLanguage.java)

rest : double → Music (MusicLanguage.java)

concat : Music × Music → Music (MusicLanguage.java) is our first producer operation.

All three of them are easy to implement by constructing the appropriate variant.

reading exercises

Music rep

Assume we have

import music.*;
import static music.Instrument.*;
import static music.MusicLanguage.*;

Which of the following represent a middle C followed by A above middle C?

(missing explanation)

Music notation

We will write pieces of music using a simplified version of abc notation, a text-based music format.

We’ve already been representing pitches using their familiar letters. Our simplified abc notation represents sequences of notes and rests with syntax for indicating their duration, accidental (sharp or flat), and octave.

For example:

C D E F G A B C' B A G F E D C represents the one-octave ascending and descending C major scale we played in ScaleSequence. C is middle C, and C' is C one octave above middle C. Each note is a quarter note.

C/2 D/2 _E/2 F/2 G/2 _A/2 _B/2 C'/2 is the ascending scale in C minor, played twice as fast. The E, A, and B are flat. Each note is an eighth note.

Read and understand the specification of notes in MusicLanguage.

You don’t need to understand the parser implementation yet, but you should understand the simplified abc notation enough to make sense of the examples.

If you’re not familiar with music theory — why is an octave 8 notes but only 12 semitones? — don’t worry. You might not be able to look at the abc strings and guess what they sound like, but you can understand the point of choosing a convenient textual syntax.

reading exercises

Simplified abc syntax

Which of these notes are twice as long as E/4?

(missing explanation)

Implementing the parser

The notes method parses strings of simplified abc notation into Music.

notes : String × Instrument → Music (MusicLanguage.java) splits the input into individual symbols (e.g. A,,/2, .1/2). We start with the empty Music, rest(0), parse symbols individually, and build up the Music using concat.

parseSymbol : String × Instrument → Music (MusicLanguage.java) returns a Rest or a Note for a single abc symbol (symbol in the grammar). It only parses the type (rest or note) and duration; it relies on parsePitch to handle pitch letters, accidentals, and octaves.

parsePitch : String → Pitch (MusicLanguage.java) returns a Pitch by parsing a pitch grammar production. You should be able to understand the recursion — what’s the base case? What are the recursive cases?

reading exercises

parsePitch

Which of the following are valid inputs to parsePitch that are handled by its base case, without any recursion?

(missing explanation)

Implementing the player

Recall our operation for playing music:

play : Music × SequencePlayer × double → void (Music.java) plays the piece of music using the given sequence player after the given number of beats delay.

Why does this operation take atBeat? Why not simply play the music now?

If we define play in that way, we won’t be able to play sequences of notes over time unless we actually pause during the play operation, for example with Thread.sleep. Our sequence player’s addNote operation is already designed to schedule notes in the future — it handles the delay.

With that design decision, it’s straightforward to implement play in every variant of Music.

Read and understand the Note.play, Rest.play, and Concat.play methods.

You should be able to follow their recursive implementations.

Just one more piece of utility code before we’re ready to jam: music.midi.MusicPlayer plays a Music using the MidiSequencePlayer. Music doesn’t know about the concrete type of the sequence player, so we need a bit of code to bring them together.

Bringing this all together, let’s use the Music ADT:

Read and understand the music.examples.ScaleMusic code.

Run the main method in ScaleMusic. You should hear the same one-octave scale again.

That’s not very exciting, so read music.examples.RowYourBoatInitial and run the main method. You should hear Row, row, row your boat!

Can you follow the executation of the code from calling notes(..), to returning an instance of Music, to the recursive play(..) call, and then individual addNote(..) calls? If you have trouble understanding the flow of the code, using a debugger to step through the code may help.

reading exercises

One note

Consider a Music object created by parsing abc for just the first note of Row, row, row your boat:

Music row = notes("C", PIANO);

Draw a snapshot diagram showing the state of the row object after this line of code has executed.

Click to show Snapdown help sidebar

(missing explanation)

Two notes

Now consider this Music object:

Music twoNotes = notes("C D", PIANO);

Draw a snapshot diagram showing the state of the twoNotes object after this line of code has executed.

A full snapshot diagram will be very complicated. To make it more manageable, use the following abbreviations:

  • (Note "C") for each Note object representing a pitch of C,
  • (Note "D") for each Note object representing a pitch of D,
  • and (Rest) for each Rest object.

Click to show Snapdown help sidebar

Since Snapdown is new: in the box below, please let us know any feedback you have on these two exercises and on Snapdown, especially if you found anything challenging or unintuitive:

(missing explanation)

Many notes

There are 27 notes in Row, row, row your boat.

Given the implementation, how many Music objects will be created by the notes call in RowYourBoat­Initial?

(missing explanation)

duration

What should be the result of rowYourBoat.duration()?

(missing explanation)

Music

Assume we have

import music.*;
import static music.Instrument.*;
import static music.MusicLanguage.*;

And

Music r = rest(1);
Pitch p = new Pitch('A').transpose(6);
Music n = note(1, p, GLOCKENSPIEL);
List<Music> s = List.of(r, n);

Which of the following is a valid Music?

(missing explanation)

To be continued

Playing Row, row, row your boat is pretty exciting, but so far the most powerful thing we’ve done is not so much the music language as it is the very basic music parser. Writing music using the simplified abc notation is clearly much more easy to understand, safe from bugs, and ready for change than writing page after page of addNote addNote addNote

In the next classes, we’ll expand our music language and turn it into a powerful tool for constructing and manipulating complex musical structures.