More Than Nothing

“You mean you can't take less; it's very easy to take more than nothing.”

Iteratively functional

Posted on November 6, 2007 by [ICR] under Programming, Scheme

I’ve been reading Amir’s blog for a while now, and I’ve noticed he quite often writes posts talking about the benefits of a functional programming style and of iterative development. Today, I am proud to say that I have iteratively developed some functional code.

The code is a simple unit-testing module written in Scheme in anticipation of my first programming assignment at University. The idea is to take a function, a list of arguments that the function could take and, given the function is evaluated with the given arguments, what output is expected. This can then be used on any functions I write to make sure that for a selection of normal and certain edge cases it functions properly.

My first attempt was a wonderfully functional one liner:

(define (test func arguments expected-results)
        (map equal? (map func arguments) expected-results)
)

Map is a functional idea that applies the given function to each of the elements in a list and returns a list with the results, so the expression (map odd? ‘(1 2 3 4 5)) will return (#t #f #t #f #t). If the function takes more than one argument, more than one list can be given and the nth item from each list will be used as the arguments, so (map + ‘(1 2 3) ‘(4 5 6)) gives (5 7 9).

My first try first applies the function to each of the arguments in the list, and then goes through each one and checks to see if it matches the expected-result, which is what we want. However, there is a limitation in that the function we are testing can only take one argument. You can get around this by wrapping up the function in a lambda expression, like so:

(test (lambda (arguments) (+ (car arguments) (cadr arguments))) '((1 2) (3 4)) '(4 6))

However, this is unwieldy and complicated for something which is a common need. It basically creates an inline function that extracts the first and second item in a list and adds them. Using a similar technique we can test functions with many parameters. However, we can improve on this.

(define (test func arguments expected-results)
        (map equal? (map (lambda (argument)
                                 (apply func argument)
                         )
                         arguments
                    )
                    expected-results
        )
)

Now instead of simply mapping the function to the list of arguments we map an inline function which applies the function to a list of arguments. This allows us to write tests like so:

(test odd? '((1) (2) (3) (4) (5)) '(#t #f #t #f #t))
(test + '((1 2) (3 4)) '(4 6))

Notice we have removed the need to wrap up non-unary functions in lambda expressions, but have added the need to enclose single arguments into their own, one element list. These are the sorts of trade-offs you often find yourself needing to make.

The problem now is that the expected-results are orphaned from the arguments. This means that to check you’re tests you must count along to, say, the 3rd argument list, and then again along to the 3rd expected-result to check you’ve done the test right. Far simpler and less error prone to bind the two more tightly, like so:

(test + '((1 2) 3)
    '((3 4) 7)
    '((5 6) 11)
)

Here we have a list of tests, the first element of which is a list of the arguments and the second the expected results. To allow for this, our test must be re-written like so:

(define (test func . params)
        (map (lambda (param)
                     (equal? (apply func (car param))
                             (cadr param)
                     )
             )
             params
        )
)

We now go through each test, evaluate the function with the argument list, and then check it matches the expected-value. The code is a little longer and ever so slightly harder to follow, but the way in which we use it is arguably more simplistic and less error prone.
There are further improvements that can be made such as improving the output to, for example, display the result if it does not match with what is expected. Maybe I’ll post the code if I ever do it.

Update:

I have spent a bit of time reworking the code into something that gives more expressive results whilst not compromising on the flexibility of simply returning a list of what succeeded and failed. What I came up with was to prepend whether the test succeeded to the original test, and append the actual result to the end, allowing a very simple check to see which tests failed and which succeeded (you can see this in the following code where the option is given to only print failed tests using the filter function). I then wrote some functions to print out the results in a more readable manner. I will post the code if only for completeness, but due to the addition of some limited object-orientation I rather feel it would be overkill to explain it blow by blow, especially as the core idea of algorithm remains mostly the same.

(require 'object->string)
(define (create-test func arguments expected-result)
        (list func arguments expected-result)
)
(define (create-tests func . argument-result-pairs)
        (map (lambda (argument-result-pair)
                     (create-test func (car argument-result-pair) (cadr argument-result-pair))
             )
             argument-result-pairs
        )
)
(define (test-func test)
        (car test)
)
(define (test-arguments test)
        (cadr test)
)
(define (test-expected-result test)
        (caddr test)
)
(define (create-test-result test)
        (let ((result (apply (test-func test)
                             (test-arguments test)
                      )
              )
             )
             (list (equal? result
                           (test-expected-result test)
                   )
                   test
                   result
             )
        )
)
(define (test-result-succeeded test-result)
        (car test-result)
)
(define (test-result-func test-result)
        (car (cadr test-result))
)
(define (test-result-arguments test-result)
        (cadr (cadr test-result))
)
(define (test-result-expected-result test-result)
        (caddr (cadr test-result))
)
(define (test-result-result test-result)
        (caddr test-result)
)
(define (print-test-result test-result)
        (print (string-append "Test "
                              (object->string (test-result-func test-result))
                              (if (test-result-succeeded test-result)
                                  " succeeded"
                                  " failed"
                              )
                              " ["
                              (object->string (test-result-arguments test-result))
                              " => "
                              (object->string (test-result-result test-result))
                              (if (not (test-result-succeeded test-result))
                                  (string-append " (expected "
                                                 (object->string (test-result-expected-result test-result))
                                                 " )"
                                  )
                                  ""
                              )
                              "]"
               )
        )
        (test-result-succeeded test-result)
)
(define (print-test-results test-results print-all)
        (if print-all
            (map print-test-result test-results)
            (map print-test-result (filter (lambda (test-result)
                                                   (not (test-result-succeeded test-result))
                                           )
                                           test-results
                                   )
            )
        )
)
(define (test tests)
        (map (lambda (test)
                     (create-test-result test)
             )
             tests
        )
)

One interesting thing to note is that with the addition of the object-orientation it made sense to more tightly bind the function being tested with the arguments and expected outputs. However, in the frequent case of applying more than one test to the same function to avoid having to repeat the function over and over I have written a create-tests function who’s syntax more closely mimics my original idea. The code can now be invoked as follows:

(print-test-results (test (create-tests + '((1 2) 3)
                                                       '((3 4) 7)
                                                       '((5 6) 11)
                                 )
                          )
                          #f
)

Not quite as succinct as the original, again displaying the decisions one has to make when programming.

Leave a Reply

Optional fields

You must type a comment