Sam
Sam

Reputation: 1132

How to 'expand' identifiers in Scheme

I'm currently working through 'The Little Schemer'. I've written some functions as per the book but I'd also like to write some unit tests for them.

I want to create a list of pairs, each pair containing a function with arguments, and it's expected output. Then I'll recur through this list and check that every evaluated function matches it's expected output. If any one of them does not match, the entire thing should return false.

This will be easier demonstrated with a code example:

(define (test-fns list-of-tests)
  ; takes a list of pairs (functions and expected outputs) and returns true only
  ; if all outputs match expected
  (define (test-fn test)
    (let
      ([fn (car test)]
       [expected (car (cdr test))])
      (equal? (eval fn) expected))
  )
  (cond
    ((null? list-of-tests) #t)
    (else
      (and (test-fn (car list-of-tests)) (test-fns (cdr list-of-tests))))))

(let
  ([lat '(ice cream with fudge for dessert)]
   [lat2 '(coffee cup tea cup and hick cup)])
  (test-fns '(
    ((multisubst 'brown 'cup lat2) '(coffee brown tea brown and hick brown))
    ((multiinsertL 'brown 'cup lat2) '(coffee brown cup tea brown cup and hick brown cup))
    ((multiinsertR 'brown 'cup lat2) '(coffee cup brown tea cup brown and hick cup brown))
    ((multirember 'cup lat2) '(coffee tea and hick))
    ((subst 'topping 'fudge lat) '(ice cream with topping for dessert)))))

However, this gives me an error: multisubst: unbound identifier;

I used DrRacket to step through the execution and realised that I am passing the symbol '(multisubst 'brown 'cup lat2) and of course lat2 is not defined inside test-fns.

So what I want to do in the second code block is somehow 'expand' each local variable (e.g. lat and lat2) into its component parts inside the list passed to test-fns.

So we would end up with (multisubst 'brown 'cup '(coffee cup tea cup and hick cup)) instead for the car of the first pair.

I have a vague notion that this might be what macros are for but I'm not sure. Any ideas?

--

V2

So I applied quasiquote as per a previous answer, and it worked (sorta). I simplified the testing code to make it easier to replicate.

Here's the new code:

(define (test-fns list-of-tests)
  ; takes a list of pairs (functions and expected outputs) and returns true only
  ; if all outputs match expected
  (define (test-fn test)
    (let
      ([fn (car test)]
       [expected (car (cdr test))])
      (equal? (eval fn) expected))
  )
  (cond
    ((null? list-of-tests) #t)
    (else
      (and (test-fn (car list-of-tests)) (test-fns (cdr list-of-tests))))))

(let
  ([lat '(coffee cup tea cup and hick cup)])
  (test-fns `(
    ((identity (quote ,lat)) '(coffee cup tea cup and hick cup)))))

This still fails with multisubst: unbound identifier; but this time I have no idea why. In the DrRacket debugger, we see the following:

fn => (identity (quote (coffee cup tea cup and hick cup)))
expected => (quote (coffee cup tea cup and hick cup))
test => ((identity (quote (coffee cup tea cup and hick cup))) (quote (coffee cup tea cup and hick cup)))
list-of-tests => (((identity (quote (... ... ... ... ... ... ...))) (quote (coffee cup tea cup and hick cup))))

So fn is actually defined just fine. Yet if I try to evaluate it directly with (fn) I get:

application: not a procedure;
 expected a procedure that can be applied to arguments
  given: '(identity '(coffee cup tea cup and hick cup))
  arguments...: [none]

and if I try to (eval fn) I get identity: unbound identifier; also, no #%app syntax transformer is bound in: identity

Perhaps my approach is fundamentally wrong, and there is a better way I should be structuring this?

--

V3 - Solved!

OK, for my final attempt I simplified this way down. I now have this:

(define (test-fns list-of-tests)
  ; takes a list of pairs (functions and expected outputs) and returns true only
  ; if all outputs match expected
  (define (test-fn test)
    (let
      ([fn (car test)]
       [args (cadr test)]
       [expected (caddr test)])
      (equal? (fn args) expected))
  )
  (cond
    ((null? list-of-tests) #t)
    (else
      (and (test-fn (car list-of-tests)) (test-fns (cdr list-of-tests))))))

Examples:

(test-fns (list (list identity '(coffee cup tea cup and hick cup) '(coffee cup tea cup and hick cup))))
> #t

(test-fns (list (list identity 1 0)))
> #f

And, introducing quasiquote into the mix allows us to expand a local lat variable in place:

(let ([lat '(coffee cup tea cup and hick cup)])
  (test-fns (list (list identity `,lat '(coffee cup tea cup and hick cup))
                (list identity 0 0))))
> #t

Yay!

--

V4 - The Final Solution (I promise)

So one problem remains with the above - we can only supply one argument to a function. The final tweak uses apply and a list of arguments like so:

;tests for chapter 3
(define (test-fns list-of-tests)
  ; takes a list of pairs (functions and expected outputs) and returns true only
  ; if all outputs match expected
  (define (test-fn test)
    (let
      ([fn (car test)]
       [args (cadr test)]
       [expected (caddr test)])
      (equal? (apply fn args) expected))
  )
  (cond
    ((null? list-of-tests) #t)
    (else
      (and (test-fn (car list-of-tests)) (test-fns (cdr list-of-tests))))))

; tests for the tester
(let ([lat '(coffee cup tea cup and hick cup)])
  (test-fns (list (list identity `(,lat) '(coffee cup tea cup and hick cup))
                  (list identity '(0) 0))))

; tests for chapter 3
(let
  ([lat '(ice cream with fudge for dessert)]
   [lat2 '(coffee cup tea cup and hick cup)])
  (test-fns (list (list multisubst `(brown cup ,lat2) '(coffee brown tea brown and hick brown)))))

Output

>#t
>#t

Hooray, it works!

--

V5 - Doing it without the quasiquotes

As per LePetitPrince's answer below, the quasiquotes are actually completely unnecessary. We can call the tester passing the identifiers directly, like this:

; tests for the tester
(let ([lat '(coffee cup tea cup and hick cup)])
  (test-fns (list (list identity (list lat) '(coffee cup tea cup and hick cup))
                  (list identity '(0) 0))))

; tests for chapter 3

(let
  ([lat '(ice cream with fudge for dessert)]
   [lat2 '(coffee cup tea cup and hick cup)])
  (test-fns (list (list multisubst (list 'brown 'cup lat2) '(coffee brown tea brown and hick brown)))))

Output:

>#t
>#t

Upvotes: 2

Views: 270

Answers (1)

uselpa
uselpa

Reputation: 18917

You can use quasiquote, or just list (I prefer the latter). Also, I don't see the reason to use eval. Here's an example:

(define (test-fns list-of-tests)
  (or (null? list-of-tests)
      (let ((test (car list-of-tests)))
        (let ((fn (car test)) (params (cadr test)) (expected-result (caddr test)))
          (let ((real-result (apply fn params)))
            (printf "~a ~a ->  ~a  ->  ~a\n" fn params expected-result real-result)
            (and (equal? expected-result real-result)
                 (test-fns (cdr list-of-tests))))))))

(Note that I'm using printf for convenience. Depending on your implementation you might have to break that down into display and newline calls.)

Testing:

(test-fns (list (list add1 '(1) 2)
                (list sub1 '(0) -1)
                (list + '(1 2 3) 6)))

yields

#<procedure:add1> (1) ->  2  ->  2
#<procedure:sub1> (0) ->  -1  ->  -1
#<procedure:+> (1 2 3) ->  6  ->  6
#t

and

(test-fns (list (list add1 '(1) 3)
                (list sub1 '(0) -1)
                (list + '(1 2 3) 6)))

yields

#<procedure:add1> (1) ->  3  ->  2
#f

Upvotes: 1

Related Questions