The Little Schemer: Practical Concerns
I've finished working my way through The Little Schemer, plus many of the exercises from The Little Lisper. One of the first hurdles I ran into was organization--as I do the coding involved in these exercises and problems, how could I configure my code suite to meet my learning needs? In rough order, here are my needs:
Attempt #1: Git 'er done!
First off, it all had to go into code management (this was CVS when I started the project, subversion later on). My first thought for the file structure was to combine function definitions and relevant tests in a single file, e.g.:
OK, the code and tests would have to go in different files. Function definitions go in lib/ (e.g. lib/atom.scm) and tests go in t/. Now how to load the functions from the test files? A little poking around in Guile's documentation yielded this mess:
This scales poorly with the number of functions and tests. Even running a few dozen tests was painful--I needed something better!
Attempt #3: TAP and runtests
At this point the number of functions and tests went through the roof. My eyes were watering from scanning all the test output, so I turned to a tool I was already familiar with from working with Perl: the Test Anything Protocol ("TAP"). TAP is a machine-friendly format for displaying test results. Here's an example of TAP output generated by my Little Schemer codebase:
If I pursue more Scheme programming (most likely SICP or SICM), my Scheme/TAP interface code could use the following:
Putting the following in ~/.guile (this is documented in the Guile documentation):
- I want to run the code in the book so that I can test my understanding.
- I want to be able to extend the book's code with my own as needed (e.g. write a function derived from one in the book).
- I want to minimize typing--I want to copy code samples from the text at most once.
- I want to re-use functions I've tested as building blocks for more complicated functions.
- Chapters in the book are made up of numerous entries of the form "what is (rember a lat)?", each building on previous entries. I want some sort of testing framework in place so that I can be certain that my code is in agreement with the text.
- I want this testing framework to expand as needed to include additional tests that I may need to understand a function's behavior.
- It would be nice if the test framework supported batching the tests and aggregating the results.
- The whole thing has to be in some sort of version control.
Attempt #1: Git 'er done!
First off, it all had to go into code management (this was CVS when I started the project, subversion later on). My first thought for the file structure was to combine function definitions and relevant tests in a single file, e.g.:
(define atom?This was sufficient for about the first half of the book--few functions are introduced, and they have few dependencies and tests. But once the more complicated functions are introduced, this style raises all sorts of problems:
(lambda (x)
(and (not (pair? x)) (not (null? x)))))
(display (atom? 'foo))
(display (atom? 42))
(display (not (atom? '(a b c))))
- Any attempt to re-load the code will re-run the tests. What a mess!
- The system is starting to scale poorly. How am I going to test the code in later chapters?
OK, the code and tests would have to go in different files. Function definitions go in lib/ (e.g. lib/atom.scm) and tests go in t/. Now how to load the functions from the test files? A little poking around in Guile's documentation yielded this mess:
(set! %load-path (cons "../lib" %load-path))But a little more poking around uncovered guile's -L command-line option; now I could invoke tests from the command line like this:
(set! %load-path (cons "lib" %load-path))
% guile -L lib t/test-atom.scmWhere test-atom.scm would look like this:
(load-from-path "atom.scm")At this point, I found the Little Lisper exercises and tried some of them. I'd solved the worst of the code reuse problem, but now I had even more tests to manage. Eyeballing the output from some shell command was too painful, even after cobbling together a makefile to handle the most common commands
; ... tests go here ...
This scales poorly with the number of functions and tests. Even running a few dozen tests was painful--I needed something better!
Attempt #3: TAP and runtests
At this point the number of functions and tests went through the roof. My eyes were watering from scanning all the test output, so I turned to a tool I was already familiar with from working with Perl: the Test Anything Protocol ("TAP"). TAP is a machine-friendly format for displaying test results. Here's an example of TAP output generated by my Little Schemer codebase:
% guile -L lib t/00-atom.scmAnd here's the Scheme code that generated it:
1..3
ok
ok
ok
%
(load-from-path "test-tap.scm")Here's test-tap.scm, which defines a handful of TAP utility functions (the interface was stolen shamelessly from Perl's test modules):
(load-from-path "atom.scm")
(plan 3)
(is (atom? 'abc) #t)
(is (atom? '()) #f)
(is (atom? '(foo)) #f)
(define planNow that our tests are generating proper TAP, we can use the runtests script (provided by Perl's TAP::Parser module) to run suites of tests and aggregate the results:
(lambda (x)
(display '1..)
(display x)
(newline)))
(define diag
(lambda (msg)
(display "# ")
(display msg)
(newline)))
(define display-ok
(lambda ()
(display "ok\n")))
(define display-not-ok
(lambda (actual correct)
(display "not ok\n")
(diag (string-concatenate (cons "actual: " (cons (object->string
actual) '()))))
(diag (string-concatenate (cons "correct: " (cons (object->string
correct) '()))))))
;; for now use "equal?" as equality test
(define is
(lambda (s1 s2)
(cond
((equal? s1 s2) (display-ok))
(else (display-not-ok s1 s2)))))
(define isnt
(lambda (s1 s2)
(cond
((equal? s1 s2) (display-not-ok))
(else (display-ok)))))
% runtests --exec 'guile -L lib' t/00-atom.scmHere's the result of running my whole test suite:
t/00-atom........ok
All tests successful.
Files=1, Tests=3, 0 wallclock secs (0.09 cusr + 0.03 csys = 0.12 CPU)
%
% find t -name '*.scm' | runtests --parse --exec 'guile -L lib' -Next steps
t/00-atom.................ok
t/ch01/00-atom............ok
t/ch02/00-lat.............ok
t/ch02/01-member..........ok
t/ch03/00-rember..........ok
t/ch03/01-firsts..........ok
t/ch03/02-seconds.........ok
t/ch04/00-add1............ok
t/ch04/01-sub1............ok
t/ch04/02-zero............ok
t/ch09/20-y-length........ok
t/ch10/00-load............ok
t/ch10/05-cond............ok
t/ch10/10-closure.........ok
All tests successful.
Files=14, Tests=44, 2 wallclock secs (0.98 cusr + 0.70 csys = 1.69 CPU)
If I pursue more Scheme programming (most likely SICP or SICM), my Scheme/TAP interface code could use the following:
- better test planning (i.e. a more sophisticated plan function)
- numbered tests
- release of the TAP output code as a proper Scheme module
Putting the following in ~/.guile (this is documented in the Guile documentation):
(use-modules (ice-9 history))
(use-modules (ice-9 readline))
(activate-readline)
Comments