There are generally four ways to process a list: iteration, traditional recursion, tail recursion, and pipelines.
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.
def duration(score: List[Note]) = sum of durations of each valid note in score
def duration(score: List[Note]) = {
var result = 0.0
for(note <- score if 0 <
note.duration)
result = result + note.duration
result
}
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)
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)
}
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(_ + _)
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.
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.
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
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
}
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)
def amplify(score:
List[Note]): List[Note] = {
var result: List[Note] = Nil
for(note <- score) result =
result :+ amplifyNote(note)
result
}
def amplify(score:
List[Note]): List[Note] =
if (score == Nil) Nil
else amplifyNote(score.head)::
amplify(score.tail)