Learning Programming: Dissecting a Turd

As mentioned in my introduction to this series, learning to program is a series of lurching steps forward, sideways, and back, leading (if all goes well) to a better holistic understanding of the process.

In many rationalist disciplines, there are learning exercises that require grokking the whole and the parts in an iterative process, until a fullness is achieved. As learning occurs, one filters out more or the irrelevant detail. Think of learning to drive, or to speak a new language, or music theory. Without knowing “the point”, the big picture, the details are just noise, and tedia. Most programming (cf: math, stats) books and courses stay focused on the details far too long. What’s missing are activities that combine the micro and macro views fluidly. Code analysis fits the bill nicely.

One of the unsung marvels of the open-source movement is that it exposes mountains of good and bad code for all to see, of every imaginable pattern and anti-pattern.  Some of the code even works, despite of (or because of) its ugliness.

Learning to read others’ code is an under-explored pedagogy tool. It’s well and good to read the sort of well-commented, rational, toy examples that one finds in books, but it’s quite another thing entirely to learn to read other peoples’ eccentric, human-created code, warts and all.

Python has some nice advantages for this sort of exercise*. It has an interactive interpreter, and even the worst python code is still pretty easy to pick apart, for most reasonable length examples, using common modules. Give me Twisted, and I can give you write-only code, but that’s my failing, not the tool.

So, as my service / penance for all the bad code I’ve unleashed, I’ve decided to comment and describe the weird mishmash of ideas and patterns existing in a first generation, but working, application I wrote.

About the code:

fret.py is a simple command line program I wrote to help me learn the notes of the guitar. I was doing the (simpler, rational) task of making flashcards, one for each fret**, when my imp of the perverse ordered me to make a computer program to do the same thing***.   I gave myself an hour to do it (which stretched into 90 minutes once I got the idea for this article), and I forced myself to leave working code alone, even if it was ugly. Especially if it was ugly.


DISSECTING THE TURD

In analysing code, one should be working on a number of fronts simultaneously.

  1. What’s the code supposed to do?
  2. What are individual statements doing?
  3. What does the code actually do?

The analyst brings things to the table

  • have I seen code like this before, in other programs, or in other sections of the codebase?
  • do I have a spec, or expectations about what is supposed to be happening?
  • are there patterns or idioms that are common for these tasks?

the code for fret.py

import random
import re

answers = {
    0:  "e a d g b e".split(),
    1:  "f a# d# g# c f".split(),
    2:  "f# b e a c# f#".split(),
    3:  "g c f a# d g".split(),
    4:  "g# c# f# b d# g#".split(),
    5:  "a d g c e a".split(),
    6:  "a# d# g# c# f a#".split(),
    7:  "b e a d f# b".split(),
    8:  "c f a# d# g c".split(),
    9:  "c# f# b e g# c#".split(),
    10:  "d g c f a d".split(),
    11:  "d# g# c# f# a# d#".split(),
    12:   "e a d g b e".split(),
}

def notequiz(*args):
    q = random.choice(range(13))
    s = random.choice(range(6))
    while 1:
        ans = raw_input("string %i fret %i?  " % (5 - s + 1,q)).strip().lower()
        ans = re.sub("\s","",ans, re.I)
        if ans == answers[q][s]:
            print "Excellent!"
            q = random.choice(range(13)) # new question
            s = random.choice(range(6))
        elif ans == "q":
            print answers[q][s]
            break
        else:
            print "No, try again, or type 'q'"

def quiz(*args):
    q = random.choice(range(13))
    while 1:
        ans = raw_input("fret %i?  " % q).strip().lower()
        ans = re.sub("\s","",ans, re.I)
        if ans == "".join(answers[q]):
            print "Excellent!"
            q = random.choice(range(13)) # new question
        elif ans == "q":
            print " ".join(answers[q])
            break
        else:
            print "No, try again, or type 'q'"

def fret(*args):
    n = int(args[0])
    try:
        print "fret %i: \t %s"  % (n, " ".join(answers[int(n)]))
    except KeyError:
        print "I don't know fret %i" % n

def help(*args):
    print '''help:
    q:  fret quiz, all strings
    n:  note quiz, fret on string
    f:  answers for a specific fret
    h:  help
'''

commands = {
    "q": quiz ,
    "n": notequiz,
    "f": fret,
    "h": help,
}

def not_in(cmd, *args):
    print "Sorry, I don't know the command %s" % cmd

######################
help()

while 1:
    cmd = raw_input("> ")
    cmd = cmd.strip().lower()
    if cmd:
        if cmd[0]=="!":
            try:
                print eval(cmd[1:])
            except Exception, e:
                print e
            continue
        try:
            c,args = cmd.split(None, 1)
            args = [args,]
        except:
            c,args=  cmd, []
        if c in commands:
            commands[c](*args)
        else:
            not_in(c)

How to analyze a program****

Strategy 1:  Top to bottom.

Note all:

  • import statements
  • names of functions
  • data structures
  • comment blocks
  • classes

Don’t worry too much about details.  Get a feel for naming conventions and the big ideas.

notes on specific lines:

1,2.  import statements, random and re.
Nothing odd here, since they’re both from the standard library.

4. “answers”: a dict of the “answers” for each fret.  This much is obvious if one knows the guitar as an instrument.  The form is kind of unusual, with the “split”  statements.  Mostly, I was too lazy to write:

0:  [“e”, “a”, “d”, “g”, “b”,”e”]

A more compSci way of handling this would have been to make a function “fretString” that calculates them on the fly for any fret.  I also should have gone up to 27 frets.

22, 40, 54, 61.  notequiz, quiz, fret, help
Each of these functions take the “*args” python special argument.

69.  commands, a dict of…. key: function pairs.  That’s odd.

76.  not_in,  a function that appears to print some kind of warning.

82.  Call the help function.

84+. a complex while loop, the heart of the code.

Strategy 2:  By Execution Order

Try to figure out what will run when the script is imported or called.  Look for definitions, assignments, __name__ == “__main__”, while loops and the like.  Try to follow these in some detail, to analyse the code.

In this code, imports happen, functions get defined, some data is filled, and the first interactive code is the help() on 82, which prints the help for the program, unsurprisingly.

Then the code from 84 onward runs.

84.  while 1  –>  loop forever
85-86. get user input and lowercase it
87-  if the user entered something
88.        and the first letter  is !
89-93.      try to run it like a normal python command.  [this is a hackish way to allow the use of normal python commands inside the guitar fret game.]
94-102.  command handling, for in-game commands
94-98. split the “command” from the “args”,
for example:  “f 12 13 14” ->  “f” ,  [“12″,”13″,”14”]
99-102.  Try to execute the user command.  This is the weirdest and least Python101 section of the whole code, since it relies on passing around functions as first class objects.

As an example:  the user enters “f 12”.  Since it starts with something not a “!”, 95 splits it into:
c = “f”
args = [“12”,]
Note that “f” is in the commands dict, so at line 100, we try to run that command.
commands[c](*args) –>   fret(12)

Then examining the code for “fret”, we see that internally it tries to return the the fret information for the first “arg” it encounters.   In this case,  fret 12.

Next Steps:

1.  Test yourself for understanding.  Imagine what would happen under various input scenarios.   How would one extend the code to handle / display flats?

2.  Create a test suite of expected answers, and verify that the program handles them correctly.

3.  Make a copy of the code (or better, put it into your version control system of choice (git, for me)) and modify it, and see what happens.

4. Decide: Does this code suck? Is it elegant? Worth stealing? Does it do it’s job? Does the interface make sense? How could the user-experience be improved?

The analyse-predict-change-measure cycle is what leads to knowledge.  One has to get ones hands dirty and risk making mistakes in order to grow as a programmer!  So, get out there, and don’t be afraid of making a turd!

FOOTNOTES

* As a rule, Write-Only is anti-flame-war, unless he starts them himself.  However, in my experience, the single greatest attribute to Python is that I can read and understand almost any published piece of python code that I’ve run across.  The indentation rules, small number of unexpected modifiers (no $, braces, $_,  etc),  simple idioms, and the like means that the right solution, the short solution, and the common solution tend to line up, making code written by good python hackers look very similar.  The crapple in this article is a blatant counter-example.

** As described in Pumping Nylon, by Scott Tennant, a perverse and wicked practice book.

*** By the way, flashcards really are much, much better, if you have a friend.  Knowing *how* to make a program to do this doesn’t mean that one should.  And if one insists, just download Solfege, which now easily support OS X!

**** Some of this is general advice, and some of it is Python-code specific.

Advertisements


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s