Design - Wed, Sep 11

Announcements

Functional Abstractions

def square(x):
    return mul(x, x)

def sum_squares(x, y):
    return square(x) + square(y)

What does sum_squares need to know about square?

What does sum_squares not need to know about square?

def square(x):              def square(x):
    return pow(x, 2)            return mul(x, x-1) + x

Even if square were bound to a built-in function, sum_squares would still work identically. The mechanisms of square are irrelevant to sum_squares.

Choosing Names: A Practical Matter

Names should convey the meaning or purpose of the values to which they are bound. The type of value bound to the name is best documented in a function's docstring.

Function names typically convey their effect (print), their behavior (triple), or the value returned (abs).

Which Values Deserve a Name?

Any of the following reasons makes it fair to add a new name:

if sqrt(square(a) + square(b)) > 1:
    x = x + sqrt(square(a) + square(b))

# avoid repeating sqrt(square(a) + square(b))
hypotenuse = sqrt(square(a) + square(b))
if hypotenuse > 1:
    x = x + hypotenuse
x1 = (-b + sqrt(square(b) - 4 * a * c)) / (2 * a)

# make the quadratic formula easier to discern
discriminant = square(b) - 4 * a * c
x1 = (-b + sqrt(discriminant)) / (2 * a)

More tips:

Test-Driven Development

Will get back to this.

Function Example: Sounds

The following is a giant example that takes everything learned so far into account. This is the remainder of today's lecture.

from wave import open
from struct import Struct
from math import floor, sin

frame_rate = 11025

def encode(x):
    """Encode float x between -1 and 1 as two bytes.
    """
    i = int(16384 * x)
    return Struct('h').pack(i)

def play(sampler, name='song.wav', seconds=2):
    """Write the output of a sampler function as a wav file.
    """
    out = open(name, 'wb')
    out.setnchannels(1)
    out.setsampwidth(2)
    out.setframerate(frame_rate)
    t = 0
    while t < seconds * frame_rate:
        sample = sampler(t)
        out.writeframes(encode(sample))
        t = t + 1
    out.close()

def tri(frequency, amplitude=0.3):
    """A continuous triangle wave."""
    period = frame_rate // frequency
    def sampler(t):
        saw_wave = t / period - floor(t / period + 0.5)
        tri_wave = 2 * abs(2 * saw_wave) - 1
        return amplitude * tri_wave
    return sampler

c_freq, e_freq, g_freq = 261.63, 329.63, 392.00

# play(tri(c_freq))

c = tri(c_freq)
e = tri(e_freq)
g = tri(g_freq)
low_g = tri(g_freq/2)

def both(f, g):
    return lambda t: f(t) + g(t)

# play(both(c, e))
# play(both(c, both(e, g)))
# play(both(c, both(e, g)), seconds=1)

def note(f, start, end, fade=.1):
    def sampler(t):
        seconds = t / frame_rate
        if seconds < start or seconds > end:
            return 0
        elif seconds < start + fade:
            return (seconds - start) / fade * f(t)
        elif seconds > end - fade:
            return (end - seconds) / fade * f(t)
        else:
            return f(t)
    return sampler

# play(note(c, 0, 1/4))
# play(both(note(c, 0, 1/4), 
#           note(e, 1/2, 1)))

def mario(c, e, g, low_g):
    z = 0
    song = note(e, z, z + 1/8)
    z += 1/8
    song = both(song, note(e, z, z + 1/8))
    z += 1/4
    song = both(song, note(e, z, z + 1/8))
    z += 1/4
    song = both(song, note(c, z, z + 1/8))
    z += 1/8
    song = both(song, note(e, z, z + 1/8))
    z += 1/4
    song = both(song, note(g, z, z + 1/4))
    z += 1/2
    song = both(song, note(low_g, z, z + 1/4))
    return song

def mario_at(octave):
    c = tri(c_freq * octave)
    e = tri(e_freq * octave)
    g = tri(g_freq * octave)
    low_g = tri(g_freq/2 * octave)
    return mario(c, e, g, low_g)

play(both(mario_at(1), mario_at(0.5)))