[Cuis-dev] New updates posted on Github
Juan Vuletich
juan at jvuletich.org
Wed Jun 26 12:04:26 PDT 2019
Hello,
I had a chance to work on a few things with Andres Valloud, here is his
summary of what we just posted on Github.
First, we turned Complex into a loadable package once more. Usually the
disadvantage is that loading such packages results in code overrides.
For instance, since the message sqrt will have to change behavior (so
that -1 sqrt returns a complex number, rather than fail outright), one
would expect that loading Complex would have to modify the base system
to introduce the new feature. This time, however, we engineered how
Complex interacts with the rest of the system so that loading the
Complex package does not result in any base system code modifications.
Of course, loading Complex causes -1 sqrt to evaluate to 0 + 1i, as before.
The key to accomplishing this was to introduce a new exception,
NegativePowerError, which by default comes in without a defaultAction
method. When Complex loads, it simply provides defaultAction so that
said exceptions are handled by invoking complex number arithmetic. We
hope this mechanism illustrates one way of making different code
packages coexist more harmoniously. Please include Complex as a
requirement to your packages as appropriate.
Second, and along the same lines, we also found that the arithmetic
error exceptions looked very similar and yet their implementation was
very different. Further, there were some discrepancies in how they were
used and handled (some would do things like ^ZeroDivide signal..., and
some others simply ZeroDivide signal...). In addition, for floating
point numbers, some of the results were not following the IEEE spec.
As an aside, it seems trivial, but even the following four operations
require *substantial* spec lawyering to get right:
0.0 - 0.0
0.0 - -0.0
-0.0 - 0.0
-0.0 - -0.0
Looks trivial --- it isn't. And then you consider +, /, and *. So, we
reorganized these exceptions under a general class called
ArithmeticMessageError. This new abstract exception has three instance
variables that will look familiar immediately: receiver, message, and
arguments. These are provided by the location where the exception
occurs, which in turn allows e.g. defaultAction to resume the
computation as required without having to either a) guess what went
wrong, or b) needing a multitude of different exceptions depending on
what actually happened. Currently, ArithmeticMessageError has two
subclasses for situations that are special enough to deserve their own
class: division by zero, and raising a negative number to powers.
In the "rationals" (i.e. Integer and Fraction), things like 1/0 are
undefined. But in floating point land, dividing aFloat by zero can be
one of three things (!): +INF, -INF, and NaN. No, this was not our
idea, but we care that things work according to the manual so that our
system enables as many folks as possible. Since floating point requires
special handling, there is a ZeroDivide exception to do so. Something
similar happens with NegativePowerError. In the "rationals", things
like -8 raisedTo: 1/3 make sense. That's because, generally,
a^(p/q) = (a^p)^1/q
Note the sign of the result, when it is defined, depends on the sign of
a and (essentially) the "parity" of p/q. Again, this was not our idea,
but we gather the world mostly works this way :).
With floating point numbers, however, it's not obvious what a^b is when
a and b are floating point numbers. Suppose a is negative, and b is 1.0
/ 3.0. Looking at that floating point 1/3, does that mean that if the
last bit of the mantissa is zero, that the result does not exist? And
that if the last bit of the mantissa is 1, then it's ok to take
something *like* the cubic root of a? What does that even mean when b
is the result of who knows how many operations, each of which is
presumably on the order of one ulp off? Thus, said powers fail, and the
result is NaN for floating point numbers.
If you are curious, consider the fantastic behavior of the following two
examples. Both fail gracefully, as they should.
-32 raisedTo: (1/5.0) asTrueFraction
-32 raisedTo: (1/5.0) successor asTrueFraction
Note that if Complex is loaded, however, a^b will happily work for
floating point numbers and produce complex numbers, as expected. For
instance, take G_6. Since -8 is 8 * w_3, then one of the possible cubic
roots is 2 * w_2 (and so is 2 * w_5).
If this behavior does not work for you, look for the relevant
defaultAction method, and also take a look at floatErrorValue. We tried
to make these moldable and easy to change so that people can configure
the system to their liking, while requiring minimal modifications.
Third, all these improvements to exceptions brought up the problem that
SUnit was catching errors by handling Error. This is not ideal because,
as just exemplified above, any exception can provide a defaultAction
method that recovers its occurrence. But handling Error like this:
[3 / 0] on: Error do: [:ex | ex return: 'everybody knows this is bad']
prevents Error from running its defaultAction, and so a recoverable
error always becomes an unrecoverable error. Instead, the above code
should say this:
[3 / 0] on: UnhandledError do: [:ex | ex return: 'evidently this error
was unexpected']
Consequently, TestResult exError should answer UnhandledError, rather
than Error. We applied this change. Please consider how you handle
exceptions with these observations in mind to maximize flexibility and
robustness.
Andres thinking the above should be at least mostly correct, Juan
approving without being too worried.
More information about the Cuis-dev
mailing list