List Processing Lab

There are generally four ways to process a list: iteration, traditional recursion, tail recursion, and pipelines.

Example: Signal Processing

In a virtual recording studio like Garage Band or Fruity Loops, a stream of notes generated by musical instruments passes through a series of amplifiers, filters, and other signal processing devices. The refined stream ends up on a master tape.

We begin with a definition and a demo:

// this is how one declares a class with three fields in Scala:
class Note(val amplitude: Double, val frequency: Double, val duration: Double = 1.0)

// a sample score for testing purposes:
val symphony1 =
   List(Note(3, 30), Note(3.1, 40, .25), Note(3.2, 10, .5),
         Note(5.1, 5, -.75), Note(3.9, 2))

Notice that the fourth note has a negative duration. This obviously doesn't make sense. Nonsense "notes" do creep in to signal processing systems in the form of noise such as crosstalk, static, interference, and distortion. We will want to skip over these "invalid" notes when processing scores.

Computing duration of a score

  def duration(score: List[Note]) = sum of durations of each valid note in score

Iterative Solution

  def duration(score: List[Note]) = {
     var result = 0.0
     for(note <- score if 0 < note.duration)
         result = result + note.duration
     result
  } 

Recursive Solution

  def duration(score: List[Note]):Double =
      if (score == Nil) 0.0
      else if (0 < score.head.duration) score.head.duration + duration(score.tail)
      else duration(score.tail)

Tail Recursive Solution

  def duration(score: List[Note]) = {
     def helper(result: Double, unseen: List[Note]): Double =
        if (unseen == Nil) result
        else if  (0 < unseen.head.duration)
             helper(result + unseen.head.duration, unseen.tail)
             else helper(result, unseen.tail)
     helper(0.0, score)
  }

Pipeline Solution

def sum(a: Double, b: Double) = a + b

def getDuration(n: Note) = n.duration

def isPositive(dur: Double) = 0 < dur

 

def  duration(score: List[Note])
   = score.map(getDuration).filter(isPositive).reduce(sum)

OR

def  duration(score: List[Note])
   = score.map(_.duration).filter(0 < _).reduce(_ + _)

Pipelines

The pipeline paradigm views list-processing algorithms as a series of processors connected by pipes:

The pipes contain the list elements, which are fed to the processors one element at a time. Each processor applies its input function to the input. In the case of a map processor the element is transformed into a different element and written to the output pipe. In the case of a filter, each element is tested with the tester function. If the element passes the test, it is written to the output pipe, otherwise it is discarded. In the case of a reducer, each element is combined with the combination of the previous elements using the combiner function. When the last element is processed, the final combination is written to the output pipe.

A pipeline may contain any number (including none) of filters and maps but usually ends with a reducer.

Pipelines are common in signal processing and big data applications.

Finding the loudest note in a score

def maxAmp(score: List[Note]): Double = amplitude of the loudest valid note in the score

Hints:

·       Copy-paste definitions of duration, renaming them maxAmp1, maxAmp2, etc. Tweak them into the desired definitions.

Iterative Solution

Recursive Solution

Tail Recursive Solution

Pipeline Solution

Filtering noise from a score

def filterNoise(score: List[Note]): List[Note] = a new score with noisy (invalid) notes eliminated

A note is noisy if it passes the following test:

def noise(note: Note) = note.duration <= 0     

Iterative Solution

The trick is to build a new score by iteratively appending the non-noisy notes

  def filterNoise(score: List[Note]): List[Note] = {
     var result: List[Note] = Nil
     for(note <- score) if (!noise(note)) result = result :+ note
     result
  } 

Recursive Solution

Tail Recursive Solution

Pipeline Solution

Amplifying a score

def amplify(score: List[Note]): List[Note] =
   a new score consisting of amplified versions of the valid notes from score

Here's how to amplify a single note:

def amplifyNote(note: Note, amt: Double = 2.0): Note =
   Note(amt * note.amplitude, note.frequency, note.duration)

Iterative Solution

  def amplify(score: List[Note]): List[Note] = {
     var result: List[Note] = Nil
     for(note <- score) result = result :+ amplifyNote(note)
     result
  }

Recursive Solution

   def amplify(score: List[Note]): List[Note] =
     if (score == Nil) Nil
     else amplifyNote(score.head):: amplify(score.tail)

Tail Recursive Solution

Pipeline Solution