racketrackunit

Create a modified `equal?` function for use with RackUnit


I am writing a bunch of tests using check-equal? with objects that contain syntax objects that are not eq?. For the purposes of these tests, I am okay with saying that two syntax objects are equal if they're equal when given to syntax->datum. (Yes, I know that looses a lot of binding information, but for now, ensuring their datums are equal is good enough. I will test that the binding information is good later.)

If the two objects are not syntax objects, however, I want them to be equal if they are equal? to each other.

However, if those objects contain syntax objects in them at some recursive point, I want to test their equality on their datums, so I can't just do:

(define (my-equal? a b)
  (if (and (syntax? a) (syntax? b)
      (equal? (syntax->datum a) (syntax->datum b))
      (equal? a b)))
(define-binary-check (check-my-equal? my-equal? actual expected))

Because this will not do the recursive check.

I could handle the recursion myself, and only use equal? on primitives, but that would be paramount to implementing my own equality testing.


Solution

  • You can use equal?/recur to get this behavior. This function behaves like equal?, but takes a different function in the case of a recursive call. So you could potentially implement my-equal? like this:

    (define (my-equal? actual expected)
      (if (and (syntax? actual) (syntax? expected))
          (equal? (syntax->datum actual) (syntax->datum expected))
          (equal?/recur actual expected my-equal?)))
    

    However, note that because you are providing the recursive function, equal?/recur does not do any cycle detection for you. As such, you need to do it yourself. One easy way to do this is with parameters:

    (define current-equal-visited (make-parameter set))
    (define (my-equal? actual expected)
      (cond [(set-member? (current-equal-visited) (cons actual expected))
             #t]
            [(and (syntax? actual) (syntax? expected))
             (equal? (syntax->datum actual) (syntax->datum expected))]
            [else
             (parameterize ([current-equal-visited
                             (set-add (current-equal-visited) (cons actual expected))])
               (equal?/recur actual expected my-equal?))]))
    

    And then, of course, as you noticed in your question, you can use define-binary-check to turn this procedure into something RackUnit can you.

    (define-binary-check (check-my-equal? my-equal? actual expected))