SICP 2.5.2 Solutions
May 29, 2016 11:27
Adding the ‘self-coercion’ routines causes a problem. If a matching procedure is not found in the table,
apply-generic will attempt to coerce the arguments. If a coercion exists, even if it coerces the type to its current type, then a new search for a procedure will commence with the ‘new’ types. That means that if coercion routines exist for all types, the search for a procedure will never exit.
The answer to part b, and a better explanation for the first part, can be found by looking at the relevant portion of
apply-generic. This is where coercion potentially occurs:
What can be seen here is that not only is self-coercion not necessary, since it will not improve the search for a matching procedure, but that in order to get the error result for no matching method, it is required that there are no coercion routines in the table for some types. The
cond statement itself tests for the very existence of the coercion routines, and assumes that if they exist, then the recursive call to
apply-generic will not be repeating the same values. It is only when the coercion routines do not exist (i.e.
get-coercion returns false) that the “No method” error message can result and the procedure can exit if it has not found a match.
To explicitly avoid this problem in
apply-generic, we can add a test for the types just ahead of the coercion lookup, which is the section just quoted. This is the solution I went for. An alternate approach, if you’d rather keep the error reporting in a single location, is to force the coercion routines (
t2->t1) to be false when the types match, something which could also be done in
The approach I took followed the suggested plan of attempting to coerce the arguments, one at a time, to the type of each argument. My routine creates a list of coercion lists, where each “coercion list” is a list of procedures that, when applied to the arguments, will coerce them all to one type. Technically the coercion list, when it is applied, doesn’t really care what it’s coercing to — all it does is apply each procedure to each argument, and then use that for the next check. To avoid the problem of the infinite loops, the coercion list is created only once, and instead of coercing on
apply-generic, an internal routine is used. Once a coercion list has been applied to the arguments, it will not be used again.
To create a coercion list, each argument type is used as a base to check the table for coercion routines. If no coercion routine is in the table, a placeholder of
'failed is placed in the list. Interestingly enough, this approach actually requires a ‘self-coercion’ routine since
apply-generic will simply map all arguments using the coercion list. We don’t rely on the table to provide it however, and simply check for a matching type and use the
uncoerced procedure, which does not change the argument.
After all arguments have had a coercion list created for them, the ones which failed are removed. Note that this does mean that when two arguments of the same type occur in the argument list, there will be a duplicate coercion list created, but I consider this a minor peformance hit.
The last part of the exercise asks for cases where this approach may fail, even when appropriate coercion routines exist. One situation, suggested by the hint, is when there are mixed-type operations. Since this approach coerces all arguments to only a single type, it cannot match against procedures that accept arguments of different type. Suppose an operation such as exponentiation is defined, but only if the exponent is of type
real. In that case, coercing all the argumnets to
complex type would fail, since exponentiation of
complex is not allowed.
Another situation where this fails is when there is another valid type that the arguments could be coerced to, which is not one of the arguments itself. If, for instance, the
polar numbers were not contained within the
complex type, but instead could be coerced to it, operations that mixed the two types would fail, even though they could work if properly coerced.
The raise operation for each type is not that difficult to implement. For each type, the constructor of the next higher type in the hierarchy is called, using the current value as its value. It’s merely a matter of math to create the value required for the constructor: for integer→rational, the denominator is set to 1, for rational→real, the numerator is divided by the denominator, and for real→complex, the imaginary part is 0.
raise operation is similar to the original
The table is checked to see if a raising procedure exists for that type. If so, the procedure is applied, which should return the raised value. If the procedure doesn’t exist, then the original value is returned.
raise could also have been implemented in one line by simply calling
apply-generic. However, since we’ve reached the point that
apply-generic has been modified to be more suitable for procedures with multiple arguments, it made sense to keep
raise simple, as it only ever needs one argument. This also lets us control what happens when the procedure is not found in the table.
There was no explicit indication of what to do when a complex number (or similary unraisable value) is submitted. I choose to simply return it unchanged. This approach means that a value is always returned, but it is not necessarily a raised value. Alternate sensible approaches would be to signal an error, or return a special value (e.g.
false) to show that the argument cannot be raised.
My intention here was to keep
apply-generic similar to its most recent form. This time, instead of applying a list of procedures to the arguments to get a coerced list, a new set of coerced arguments is generated. The function that generates the coerced arguments will return false if the arguments cannot be changed. Otherwise the altered set of arguments is returned, and
apply-generic is called recursively.
One distinction between this version and the previous one is that only one coerced argument list is produced each time. To ensure that all possible cases are covered, we raise a single argument at a time, only if it is of the lowest type in the list. The procedure
raise-lowest accomplishes this. It steps through each argument, and produces a list with only this argument raised, and then only if the argument’s type precedence is at or below the lowest type seen. Once all the arguments have been examined, the new argument list will only have one raised argument. If no arguments could be raised, the procedure returns
To indicate type precedence in a manner that will allow for future expansion, each type has an entry in the table which is a numeric value for its precedence. Types with higher precedence actually have a lower numeric value, and any value that is less than 0 is considered an ‘unraisable’ type (this is also the default for types that have no precedence entry in the table). This allows multiple type systems to be in place, and even allows two types to have the same precedence. Inserting a new type in place of an existing precedence level could be done by using fractional values, or by altering the table for the types that already exist.
project is rather similar to the generic
raise; the big difference here is that
false is returned when the value cannot be projected (there’s also an explict check for pairs, to avoid projecting ‘ordinary’ Scheme numbers). Each type then has its own project method in the table, and we are using the ‘rationalized’ real values so that the types raise and lower on the same steps.
That is only part of the simplification process. Generic
drop has to check that the type can be projected, and that it is also equal to the original when raised back. Here’s
Note the use of a
cond statement. We take advantage of the fact that the
cond will exit at the first matching clause to avoid attempting to raise invalid values. If
(project x) returns false, we make a check on the first statement. That way we don’t bother trying to raise something if it can’t be projected.
In similar fashion to
raise, my version of
drop returns the value itself if it could not be changed. The other notable feature is after a successful drop, the procedure recurses — this makes it continue to
drop as far as possible. At that point the value is returned, and it’s the lowest it can possibly be.
Incorporating this into
apply-generic is very easy thanks to that behavior. A simple call to
drop needs to be added at the point when a valid procedure for the given types has been found. That one call will yield an answer that is dropped as far as possible, even in cases where no actual drop is performed.
The changes here are not quite as difficult to introduce as it may appear, but doing so may reveal hidden flaws in the system if it hasn’t been built up properly. I ended up rewriting a lot of the earlier exercises thanks to bugs detected while implementing this feature.
In short, we need to change all internal procedures that use math in the complex system to ones that use generic operations. This change needs to be implemented at the level of the
polar system as well, since they internally perform differing calculations. Additionally, generic operations for all the more advanced mathematic operations (such as sine or arctangent) must be implmented. Thanks to the raise operation, the last set only needs to be defined for the reals, since values of other types will just be raised as necessary.
The only other change made is when dropping a complex value. In order to check the drop properly, the value must be forced to be of real type. To handle that, a
to-scheme-number is added that returns the ‘ordinary’ Scheme value for something of generic type. This is required since the constructor for the lower types still expect those values as arguments. This change would no longer be required if the constructor were altered to accept generic arithmetic values as well. As it happens, it would still actually work without this operation, but only because ordinary Scheme values are incorporated into the system.