Skip to content

TM016 Testing Inexact Numbers

jpolitz edited this page Oct 8, 2014 · 2 revisions

Testing Inexact Numbers

Problem

Comparing inexact numbers is a source of bugs and is difficult to test. Pyret's is%(pred) syntax provides a good option for specifying ranges of comparison when numbers are being compared directly; for example:

check:
  within = lam(delta): lam(n1, n2): num-abs(n1 - n2) < delta end end
  num-sin(1) is%(within(0.01)) 0.84
end

However, when scaling up to lists and structures that contain inexact numbers, this quickly becomes difficult:

check:
  within = lam(delta): lam(n1, n2): num-abs(n1 - n2) < delta end end
  fun within-list(delta):
    lam(l1, l2): lists.all2(within(delta), l1, l2) end
  end
  sins = map(num-sin, [list: 1, 2, 3, 4])

  sins is%(within-list(0.01)) [list: 0.84, 0.90, 0.14, -0.75]
end

For more and more complicated structures, the tester has to essentially reproduce the equality traversal for the structure in question, but replace all inexact comparisons with within rather than equality checks.

Solution

The proposed solution is twofold (and either part may be valuable on its own).

Make Inexact Numbers Incomparable

Pyret already has a notion of incomparable values, for which it refuses to report true or false in equality: functions and methods. If we add inexact numbers to the set of incomparable values, we could make comparisons on inexacts return Unknown.

We could even extend the notion of error in this case, and have any comparison of inexact numbers to exact numbers return Unknown as well (and raise the corresponding exception).

Add a Built-in within Function

Further, Pyret could get several new functions:

within-always :: Number -> (Any, Any -> Boolean)
within-always3 :: Number -> (Any, Any -> EqualityResult)
within-now :: Number -> (Any, Any -> Boolean)
within-now3 :: Number -> (Any, Any -> EqualityResult)

These two functions would take numbers (either inexact or exact), and would produce functions with the same behavior as equal-always and equal-now, with the exception that instead of raising an error/returning Unknown on inexact numbers, the two numbers would be compared for equality within the tolerance of the first Number argument.

This would re-use existing datatypes' _equals methods to do the traversal of members for e.g. user-defined collections, but parameterize the entire equality process over the user's choice of tolerance.

Datatypes that manage their own internal inexact values can always compare them with a more rigid or lenient tolerance than the one specified by the user of within-*, if that tolerance is part of the datatype's specification. However, many datatypes (especially collections) would benefit from being agnostic to this behavior and supporting its parameterization.