Here I present a short (one-page) class in Bicicleta for exact
arithmetic on rational numbers, followed by a ten-page explanation of
how it works. It's loosely based on the example at the beginning of
chapter 2 of SICP.

It is intended to stand alone, comprehensible without reference to any
of the previous Bicicleta posts I have made on kragen-tol and
kragen-hacks.

Unfortunately, I still don't have a runnable Bicicleta system, which
has several effects on this class:
- it probably has some bugs that will be obvious when I try to run it;
- I did not write unit tests, since I would not be able to run them;
- the textual source code does not have a full complement of example
  values in it.
- it's about fractions, which are boring, instead of something cool
  like colors or 3-D solid models or web sites.

The textual syntax in this mail message is not quite the syntax you
will see when you're editing the program.  Because it's so important
to be able to exchange and discuss program fragments in purely textual
media like email, I plan to fully support copy and paste in this
textual format from the Bicicleta environment, without losing any
semantics; and because it's so important to integrate with
source-control systems, I plan to use the textual format as the
on-disk persistent storage format for Bicicleta programs.  But within
the Bicicleta environment itself, the UI for interacting with the
program is not purely textual.

(In itself, this is not a large difference from traditional IDEs like
Eclipse.)

The other important thing to know is that Bicicleta, like Haskell,
pervasively uses lazy evaluation: no expression is evaluated until its
value is needed.

Things that embarrass me are marked with "***".

The Whole Class
---------------

First, here's the class all at once.  I've followed it with a series
of piece-by-piece explanations.

rational = prog.sys.number {self:
    numer = 1
    denom = 2
    new = {op: 
        arg1 = 6
        arg2 = 9
        g = op.arg1.gcd(op.arg2)
        numer = op.arg1 / op.g
        denom = op.arg2 / op.g
        '()' = (op.numer * op.denom).if_not_error(
             self { numer = op.numer, denom = op.denom })
    }
    show = "{numer} <hr> {denom}" % self     # presentational form definition
    to_rational = {op:
        arg1 = 2
        '()' = prog.if(
             op.arg1.denom.is_ok -> op.arg1
             op.arg1.is_a(prog.sys.integer) -> self.new(op.arg1, 1)
             else = prog.error()
        )
    }
    rational_binary = {m:
        arg1 = prog.sys.number.'+'
        '()' = m.arg1 {op: 3, other = self.to_rational(op.arg1)}
    }
    # prog.sys.number.'+' returns 'result' unless it's erroneous, so we
    # override 'result'
    '+' = self.rational_binary(prog.sys.number.'+') {op:
        result = self.new(
             (self.numer * op.other.denom) + (self.denom * op.other.numer)
             self.denom * op.other.denom
        )
    }
    # The theory is that the standard number types' implementations of
    # '+', '-', and the like, if the normal implementation fails with
    # an error, will try passing themselves to 'reverse +', 'reverse
    # -', and so on, instead.  The override here is to stop the
    # recursion.
    'reverse +' = self.'+' {op: '()' = op.result}
    negate = self.new(-self.numer, self.denom)
    '-' = self.rational_binary(prog.sys.number.'-') {op:
        result = self + op.other.negate
    }
    # This should not call '-', because if it does, and '-' is somehow
    # broken, we get infinite recursion.
    'reverse -' = self.'+' {op: '()' = self.negate + op.other}
    '*' = self.rational_binary(prog.sys.number.'*') {op:
        result = self.new(self.numer * op.other.numer,
                          self.denom * op.other.denom)
    }
    'reverse *' = self.'*' {op: '()' = op.result}
    recip = self.new(self.denom, self.numer)
    '/' = self.rational_binary(prog.sys.number.'/') {op: 
        result = self * op.other.recip
    }
    'reverse /' = self.'+' {op: '()' = self.recip * op.other}

    # Equality is a really tricky operation.  Here we're implementing
    # numerical equality, but I'm not sure that's the right thing.
    '==' = self.'+' {op:
         '()' = (self.numer * op.other.denom) == (self.denom * op.other.numer)
    }
}

rational = prog.sys.number {self: ... }
---------------------------------------

This says "'rational' inherits from prog.sys.number, with the
following differences: ...".  The identifier "self" as the first thing
in the {} and followed by a ":" gives a name by which methods in
'rational' can refer to the object on which they are called.
(Sometimes I refer to methods as 'fields' in what follows.)

'prog' is the name given to the top-level namespace of the program.

Several of the fields introduced inside this expression do not exist
in prog.sys.number.  (I'm not sure exactly which ones, but several of
them.)  In Abadí and Cardelli's ς-calculus, introducing new fields in
an override expression like this is against the rules, but I am
disregarding this restriction.

numer = 1, denom = 2
--------------------

This defines two methods on the 'rational' object; one of them returns
1, and the other returns 2.

These are intended to be overridden in other objects derived from
'rational', but they serve as example values that allow the
"archetypal" or "prototypical" rational object to be viewed as a real
rational number, in this case 1/2.  This allows the environment to
constantly display the effects of your changes to the code in real
time.

new = {op:...}
--------------

This defines a field (or method) whose value is another object, which
is intended as a constructor for new 'rational' objects.  Within the
body of its definition, the name 'self' still refers to the 'rational'
object on which 'new' was called, and 'op' refers to the 'new' object
itself.

        arg1 = 6
        arg2 = 9

This defines two fields whose use becomes apparent later.

        g = op.arg1.gcd(op.arg2)

This is syntactic sugar for the following definition:

        'g' = op.'arg1'.'gcd'{'arg1' = op.'arg2'}.'()'

by way of the following translations:

     1. It is allowed to leave off the '' around the name of a field
        when the field name contains only alphanumeric and underscore
        characters; (The '' allow the use of any arbitrary characters
        in field names.)
     2. In general x(y) is syntactic sugar for x{y}.'()'.  Here 'x' is
        some object, 'y' specifies some set of overrides on x to make
        a derived object, and .'()' extracts the '()' field of the
        resulting object.
     3. Positional arguments in an override expression are treated as
        overrides for the fields 'arg1', 'arg2', and so on.

Note that rule 3 means that you can write 'rational.new(2, 5)' to mean
'rational.new{arg1=2, arg2=5}'.

This says that the 'g' method on the 'new' object does the following:

     1. extracts the 'arg1' field from the 'new' object on which it is
        called;
     2. extracts the 'gcd' field from that 'arg1' object;
     3. extracts the 'arg2' field from the same 'new' object;
     4. creates an object derived from the 'gcd' object in step 2 by
        overriding its 'arg1' method to return the object from step 3;
     5. extracts the '()' field from the resulting object.

'gcd' is a method defined on integer objects; its '()' field contains
its "return value", which is the greatest common divisor of the number
on which it is called and its argument.  In this case, it is 3, but in
an object derived from 'new', arg1 and arg2 may be overridden, in
which case 'g' will have a different value.

Here is the current sugar-free notation for this field definition:

        g = op.arg1.gcd{arg1 = op.arg2}.'()'

I used to write it as this instead:

        op.g = op.arg1.gcd{arg1 = op.arg2}.'()'

This is more self-contained; but I concluded that it made more sense
to provide a single "self-name" for all the methods in a particular
expression, and I think the above was confusing to read, because most
readers did not guess that the first occurrence of 'op' introduced a
new binding for 'op' rather than referring to an existing binding.  If
I recall correctly, in the notation of Abadí and Cardelli, it is
written like this:

        g = ς(op) op.arg1.gcd{arg1 <= op.arg2}.val

which I usually expand in ASCII like this:

        g = sigma(op) op.arg1.gcd{arg1 <= op.arg2}.val

where we write 'val' for '()' because they don't have a notation for
non-alphanumeric method names.

Note that the above does not constrain arg1 and arg2 to be integers;
arg1 just has to have a 'gcd' field which has a '()' field.

        numer = op.arg1 / op.g
        denom = op.arg2 / op.g

These divide the arg1 and arg2 by the gcd, giving 2 and 3
respectively.  They are syntactic sugar for definitions like this:

        numer = op.arg1.'/'(op.g)

which in turn is sugar for

        'numer' = op.'arg1'.'/'{arg1 = op.'g'}.'()'

Any sequence of characters from this sequence will be interpreted in
this fashion as an infix operator:

        [EMAIL PROTECTED]&*-+=<>?/\|

So you can define a ** operator or a += operator or a @!@@ operator by
defining fields on your objects with those names.  There is no
precedence among these infix operators, and they all associate from
left to right; but, probably needless to say, the built-in "." and
override syntax bind more tightly than these infix operators, and the
separation between items expressed by "," or a line break binds more
loosely.

        '()' = (op.numer * op.denom).if_not_error(
             self { numer = op.numer, denom = op.denom })

This field, by convention supported by syntactic sugar, holds the
"return value" of 'new' --- so if you say 'rational.new(3, 4)' you are
getting the contents of this '()' field.

The 'if_not_error' method is defined on all objects.  For normal
objects, it just returns its argument:

         if_not_error = {op: '()' = op.arg1}

But for error objects, which result from calls to nonexistent methods
(among other things), it returns the error object itself:

         if_not_error = { '()' = self }

The intent of the if_not_error call is to catch cases where op.numer,
op.denom, or both, are error objects, or where they are not the sort
of thing you can use as rational numerators or denominators.  If
op.numer is an error object, its '*' method will just return another
error object; and the intrinsic '*' method defined for integers will
also return an error if passed an error object as an argument.
Finally, if op.numer is some kind of thing that doesn't have an '*'
method, or whose '*' method cannot accept op.denom as an argument, we
will get an error object reporting this, instead of a rational
number.  (Being able to multiply together numerators and denominators
is a necessary condition for arithmetic on rational numbers, and it
should catch nearly all cases where the wrong thing was passed in.)

Error objects propagate along dataflow paths, as in VisiCalc and other
spreadsheets, rather than control-flow paths, as exceptions do in CLU
or Java.  This is in part because the control-flow paths in a lazy
program are very hard to understand, but my experience with such a
mechanism in Wheat leads me to believe that this is a reasonable
approach for an imperative language as well, as long as there's an
escape hatch for error values evaluated in void context "for effect",
so they don't get lost.

This expression is a horizontal layout form:

             self { numer = op.numer, denom = op.denom }

It's exactly equivalent to this:

             self {
                 numer = op.numer
                 denom = op.denom
             }

This constructs an object similar to the one on which 'new' is being
called, but with different 'numer' and 'denom' fields.

     At one point in the past, I argued that constructions like this,
     where the kind of object you instantiate covaries with the kind
     of object you were operating on, were dead wrong.  I was wrong.

This points out that you don't have to construct rationals through the
'new' method.  You could also say "rational { numer = 3, denom = 4 }"
and have a perfectly usable 'rational' object.

The reason I'm providing the 'new' method is that it provides a useful
level of indirection.  "rational { numer = 3, denom = 4 }" cannot
return an error object, nor rewrite numer and denom to lowest terms
the way 'new' does; and I found experimentally that it's quite easy to
override the wrong fields and introduce a subtle bug.

It's worth noticing that you could use 'new' to construct rationals
from any kind of object that supports 'gcd', '/', and '*' methods that
interact in the expected way; and, if they support the other ways that
numerators and denominators are used in this class (adding their
products together, negating them, testing for equality), they will
work, too.  For example, I think you could use this class unchanged to
support rational expressions of polynomials.

show = "{numer} <hr> {denom}" % self
------------------------------------

By convention, 'show' defines an HTML representation for the number
using the '%' method of strings, which uses reflection to extract the
fields named inside {} and interpolate them into a string.
Traditionally, programming languages have ways to render their objects
into ASCII strings, for debugging purposes if nothing else, but I
think HTML has supplanted ASCII text today.  "show" is what the
Bicicleta environment uses to display the current values of the
objects you're editing.

I anticipate that for many classes, "show" or something like it will
suffice as a user interface.

*** This is not the right way to generate HTML.  I probably want
something like Nevow stan or MochiKit.DOM, such as
"prog.html(self.numer, prog.html.hr, self.denom)", but I need more
experience to design that.

'+' = self.rational_binary(...) {op: ...}
-----------------------------------------

    '+' = self.rational_binary(prog.sys.number.'+') {op:
        result = self.new(
             (self.numer * op.other.denom) + (self.denom * op.other.numer)
             self.denom * op.other.denom
        )
    }

This defines a method for '+', which inherits from
self.rational_binary(prog.sys.number.'+'), which (as we'll see below)
inherits from prog.sys.number.'+'.  Rather than defining '()', which
is what is actually used in an expression like "r1 + r2" (remember,
that rewrites to "r1.'+'{arg1=r2}.'()'") we define 'result', which the
'()' we inherit from prog.sys.number.'+' will return under most
circumstances.

Due to the lack of operator precedence in Bicicleta, we can't just
write "numer * other.denom + denom * other.numer"; that would parse as
"((numer * other.denom) + denom) * other.numer".  So there's a set of
parentheses on the right to make it parse correctly, and another one
on the left for symmetry and clarity.

'other', as explained below, comes from 'rational_binary' and contains
a rational-number version of the argument, whether or not the argument
was originally a rational number.

to_rational = {op: ... '()' = prog.if(...)}
-------------------------------------------

    to_rational = {op:
        arg1 = 2
        '()' = prog.if(
             op.arg1.denom.is_ok -> op.arg1
             op.arg1.is_a(prog.sys.integer) -> self.new(op.arg1, 1)
             else = prog.error()
        )
    }

Most of the rest of the class is concerned with arithmetic.  It's
desirable to be able to mix rational numbers with integers in
arithmetic; so this method returns a rational number, given either a
rational number or an integer.

The '->' method is defined for all objects; it returns an
'association' containing the object it was called on and its argument.

'if' is defined at 'prog', the top level; it implements a multi-way
conditional similar to Lisp's "cond".  You pass it any number of
associations as arguments, and it iterates over them until it finds an
association whose left-hand side is true, at which point it returns
the right-hand side.  If none of them are true, it returns 'else', in
this case, an error object.  (*** I probably ought to provide an error
message.)

    Note that the definition of 'if' in this fashion requires access
    to the whole list of positional parameters, not just particular
    individual positional parameters.

Because the language is lazy, 'if' can be just an ordinary function
rather than a language special form.

It is unfortunate that I have to write "prog.if" rather than "if" in
the textual syntax.  This is because only the objects textually
enclosing an expression are bound to names in its scope; in this
expression, if 'rational' is evaluated at the top level 'prog', the
environment consists only of 'prog', 'self', and 'op'.  I anticipate
that the Bicicleta environment will abbreviate these names for display
where doing so will not lead to reader confusion:

    to_rational = {op:
        arg1 = 2
        '()' = if(
             arg1.denom.is_ok -> arg1
             arg1.is_a(sys.integer) -> new(arg1, 1)
             else = error()
        )
    }

'is_ok' is a method defined on all objects; it returns true for all
objects except for error objects.  If arg1 is a rational number, then
unless its denom is an error object, arg1.denom.is_ok will be true.
But for almost all objects other than rational numbers,
arg1.denom.is_ok will be false.

This is an example of using a 'protocol test' rather than an is-a or
isinstance test.  (See my web page, "isinstance considered harmful".)
This leads to looser coupling and more maintainable systems.

Unfortunately I'm not sure how to do a protocol test to distinguish,
say, integers, which can be converted to exact rational numbers in the
way suggested above, from, say, floating-point numbers, which cannot.
Probably it will be clear how to do this after I've implemented some
of the more basic kinds of numbers.  But in the mean time, I've fallen
back on "op.arg1.is_a(prog.sys.integer) -> self.new(op.arg1, 1)".

    rational_binary = {m:
        arg1 = prog.sys.number.'+'
        '()' = m.arg1 {op: 3, other = self.to_rational(op.arg1)}
    }

The methods that want to convert something else to a rational number
are all binary arithmetic methods of one kind or another, and the
thing they want to convert to rational is their argument.  The trouble
is, for other reasons explained below, they need to inherit from their
corresponding methods in prog.sys.number, and because Bicicleta has
neither multiple inheritance nor super and therefore we cannot simply
implement "other = to_rational(arg1)" as a mixin.

However, this method demonstrates that it's straightforward to get
mixin-like functionality from a function.  This function adds arg1=3
and the "other" method to its argument, and returns it.

In retrospect, this method probably didn't make the code much simpler,
but I've left it in because it demonstrates this important point about
the need for mixins.

*** Perhaps the "try to coerce to my type" behavior should be
inherited from prog.sys.number operations instead of implemented in
every numeric type.

At this point we have all the pieces to know that rational.'+'() is
7/2, because the default argument provided by rational_binary is 3.

*** I'm not sure about whether I should be providing reasonable
default arguments like this, which makes the code clearer when you're
editing it and seeing operational results, or defaulting to error
values so that when you use the class you get error messages instead
of silent wrong answers.  I'm thinking that perhaps the first is
better during initial development, while the second is better later
on.

    'reverse +' = self.'+' {op: '()' = op.result}

Earlier I said that prog.sys.number.'+' would return 'result' under
"most circumstances".  I meant that it does the following:

    '+' = {op: '()' = op.result !! op.arg1.'reverse +'(self)}

The '!!' error-handling operator comes from Wheat.  It's like '||',
but for non-broken-ness rather than truthiness.  In normal objects,
it's defined as follows:

     '!!' = { '()' = self }

But in error objects, it's defined as follows:

    '!!' = {op: 
         '()' = op.arg1 !! self.augment_err("trying to recover from", op.arg1)
    }

So, if 'result' isn't an error, '+' returns it; if it is, it tries
arg1.'reverse +'(self), and if that isn't an error, it returns that;
and if they're both errors, it returns an error containing both
errors.

*** That won't actually work as written because self.augment_err
returns an error object.  It should probably be written out as an if.

In this case, the way this works is that in "1 + rational.new(1, 2)",
we try 1.'+', which presumably will try and fail to coerce the
rational number to an integer; then it will fall back on
"rational.new(1, 2).'reverse +'(1)".

At first I tried this:

    'reverse +' = self.'+'

This works in the normal case, where you're trying to add two rational
numbers, or a rational number and an integer (in either order).  But
if there's an error (say, if you try to add 0.5 to a rational number)
then our 'reverse +' operator goes right ahead and tries to call
0.5.'reverse +' again.  Which is OK, because that fails, and we get
a relatively sensible error.

But if there's some kind of bug in rational.'+' that makes 'result'
always an error for two particular rationals, then we'll fall into an
infinite recursive loop trying to add them, as they futilely swap back
and forth trying to find a configuration that works.  This will result
in an error message that is considerably less helpful than it could
be.

So we inherit 'reverse +' from rational.'+', but we override '()' to
return 'result' directly instead of trying to reverse places again.

    negate = self.new(-self.numer, self.denom)

This returns a negative version of the number (unless it's already
negative, in which case it returns a positive version).  Its value is
-1/2 in the prototypical rational object.

*** The "-" in here doesn't fit into the syntactic structure I
described earlier for infix operators, and as a consequence, there's
no obvious way to override it.  I am inclined to write this as
"self.numer.negate" instead and banish prefix operators completely
from the language.

    '-' = self.rational_binary(prog.sys.number.'-') {op:
        result = self + op.other.negate
    }
    'reverse -' = self.'+' {op: '()' = self.negate + op.other}

'-' overrides 'result' because '()' inherits a fallback to 'reverse -'
from prog.sys.number.'-', and 'reverse -' overrides '()' rather than
'result' in order to avoid falling back.

I inherited 'reverse -' from self.'+' to avoid having to write out the
rational_binary or to_rational conversion again (and potentially
forgetting).  This has the perverse side effect that 'reverse -'
inherits an unused 'result' field containing the sum of 'self' and
'other'.

The 'reverse -' could still lead to an infinite recursive loop if
someone decided to implement '+' in terms of '-' (perhaps in some
other class), but it's not likely.

*** It would be nice if new programmers didn't have to deal with this
level of subtlety just to define a new numeric type.  They don't in
Python.  Maybe a level of indirection like "rational.new" could help?
Maybe something you could wrap around the whole class, like
"rational.rational_binary" wraps around a single operation.

    '*' = self.rational_binary(prog.sys.number.'*') {op:
        result = self.new(self.numer * op.other.numer,
                          self.denom * op.other.denom)
    }
    'reverse *' = self.'*' {op: '()' = op.result}

Perhaps I should define a 'commutative_reversed' method so that I
could say 'reverse *' = self.commutative_reversed(self.'*').

    recip = self.new(self.denom, self.numer)
    '/' = self.rational_binary(prog.sys.number.'/') {op: 
        result = self * op.other.recip
    }
    'reverse /' = self.'+' {op: '()' = self.recip * op.other}

Follows the same patterns as previously described for '-'.

    '==' = self.'+' {op:
         '()' = (self.numer * op.other.denom) == (self.denom * op.other.numer)
    }

This definition is tricky because there are lots of different kinds of
equality: numerical equality, structural equality, and identity
equality are the ones we have to worry about here.  Here I've picked
numerical equality as the definition for rational.'==', with the
consequences that "rational.new(2, 1) == 2" is true, and
"rational.new(2, 1) == 0.1" returns an error instead of false.

If our 'new' constructor (or gcd?) were smart enough to ensure that
denom was never negative, perhaps we could reduce this to "(numer ==
other.numer) && (denom == other.denom)".

*** '==' probably should use the same coercion techniques as '+' and
family if we're interested in numerical equality, so that we can test
"2 == rational.new(2, 1)".

The Whole Error Object Protocol
-------------------------------

Bicicleta has Wheat-style error objects, instantiated with
prog.error(), but they're just ordinary objects that implement only a
few methods:
- is_ok = false (true for other objects)
- if_not_error = {'()' = self} (arg1 for other objects)
- '!!' = {op: '()' = op.arg1 and a bit more} (self for other objects)
- show: something handy with hyperlinks
- get_error_info: returns the information about the object

There are some other methods I haven't really defined, like the
"augment_err" mentioned above.  Possibly I should hide them behind an
"as_error" method.

Additionally, when you call a nonexistent method on an error object,
the error object returned isn't a fresh "nonexistent method called"
error object; it's the original error object, augmented with
information about being propagated through that method call.  This is
achieved through some facility I haven't defined yet, analogous to
Smalltalk's "doesNotUnderstand:" or Python's "__getattr__".

Wheat's error objects modify themselves in place when they are passed
around.  This became confusing in practice; in Bicicleta, error
objects have a 'augment_err' method that returns a copy of the same
error object, but with more trace information hanging off of it.

Is Succinctness Power?
----------------------

If you compare the above to the version that heads SICP's Chapter 2,
you will notice that it is substantially longer, by a factor of two to
four.  I love brevity less than Paul Graham or Arthur Whitney, but I
certainly recognize brevity as precious, and I was dismayed when I
first realized how verbose Bicicleta code was.

Since that dismay, I have improved it somewhat, and now I think it's
roughly competitive with Scheme.  Most of the extra length comes from
the hassles of integrating smoothly into an existing arithmetic
system, which isn't even possible in Scheme (in the abstract, that is
--- I think it's possible in, say, RScheme or PLT Scheme.)

It's still a somewhat fluffier language, mostly because it does many
things by name that Scheme does positionally.

Here's a version of the 'rational' class that's missing all the
extraneous code, focusing only on the basics:

rational = {r:
    numer = 1
    denom = 2
    new = {op: 
        arg1 = 6
        arg2 = 9
        g = op.arg1.gcd(op.arg2)
        numer = op.arg1 / op.g
        denom = op.arg2 / op.g
        '()' = (op.numer * op.denom).if_not_error(
             r { numer = op.numer, denom = op.denom })
    }
    show = "{numer} <hr> {denom}" % r
    '+' = {op:
        '()' = r.new(
             (r.numer * op.arg1.denom) + (r.denom * op.arg1.numer)
             r.denom * op.arg1.denom
        )
    }
    negate = r.new(-r.numer, r.denom)
    '-' = {op: '()' = r + op.arg1.negate}
    '*' = {op: '()' = r.new(r.numer * op.arg1.numer, r.denom * op.arg1.denom)}
    recip = r.new(r.denom, r.numer)
    '/' = {op: '()' = r * op.arg1.recip}
    '==' = {op: '()' = (r.numer * op.arg1.denom) == (r.denom * op.arg1.numer)}
}

That's 26 lines, 812 characters.

And here's a version abbreviated in the way I think things will
normally be abbreviated for display, where the namespace name is
omitted for uniquely-named methods:

rational = {r:
    numer = 1
    denom = 2
    new = {op: 
        arg1 = 6
        arg2 = 9
        g = arg1.gcd(arg2)
        numer = arg1 / op
        denom = arg2 / op
        '()' = (op.numer * op.denom).if_not_error(
             r { numer = op.numer, denom = op.denom })
    }
    show = "{numer} <hr> {denom}" % r
    '+' = {
        '()' = new(
             (numer * arg1.denom) + (denom * arg1.numer)
             denom * arg1.denom
        )
    }
    negate = new(-numer, denom)
    '-' = {'()' = r + arg1.negate}
    '*' = {'()' = new(numer * arg1.numer, denom * arg1.denom)}
    recip = new(denom, numer)
    '/' = {'()' = r * arg1.recip}
    '==' = {'()' = (numer * arg1.denom) == (denom * arg1.numer)}
}

That's 26 lines, 720 characters.

By comparison, the SICP version is 35 lines, 788 characters, but it
has some duplication of function; if I factor it and format it in an
analogous way, it's 18 lines, 710 characters.

Some parts of the Scheme version are actually more verbose than their
Bicicleta counterparts, which is why it's possible to come so close.
Consider:

    negate = new(-numer, denom)
    negate = r.new(-r.numer, r.denom)
    negate = self.new(-self.numer, self.denom)
    negate = self.new(self.numer.negate, self.denom)
    (define (neg-rat x) (make-rat (- (numer x)) (denom x)))

    '-' = {op: '()' = r + op.arg1.negate}
    (define (sub-rat x y) (add-rat x (neg-rat y)))

So at this point I no longer have a lot of Scheme envy.  Scheme still
wins by a little bit most of the time on brevity-in-the-small and
clear applicative-order control flow, but I think Bicicleta will win
on infix notation, named arguments, better namespace management,
runtime inspectability, and easier data structuring.  Compared to
minimal standard schemes, Bicicleta will also win on late binding and
composability --- the SICP example rational class can only use
built-in numbers for its numerator and denominator.

The biggest recent improvement in brevity was not having to write the
self-name on every method that used it:

    self.negate = self.new(-self.numer, self.denom)

Things Not Shown
----------------

There are a couple of things that are part of the Bicicleta design
that haven't made an appearance here.

The biggest one is editable presentation forms.  When I'm editing my
source, I don't want rational numbers to display as "rational.new(3,
4)"; I want them to display like this:

    3
   ---
    4

But not a hokey dashed line made out of hyphens --- a solid line, with
a minimal amount of space above and below it.  I might want to switch
to "text view" to see how to make another one (the big advantage of
text is that it makes the gulf of execution quite small), but I should
be able to change the 3 to a 5 without going to that extreme.

The handling of side effects is something I have given some thought
to, but which doesn't show up here.  My plan is to make a special kind
of function called a 'procedure'; to only allow procedures to be
called from procedures; to cause UI elements to invoke procedures; to
cause the 'main' function of a standalone program to invoke a
procedure; to provide special procedures that implement iteration of a
procedure and sequencing of multiple procedures; to use 'if' to
implement a choice between N procedures (it just returns the procedure
it wants to call); and, generally, to record the last value returned
from a procedure so it can be inspected without being re-executed.

Another, much smaller, item is that "x[y]" is syntactic sugar for
"x.'[]'(y)", as in Wheat; this is used for indexing into data
containers.  This (plus conventions for escaping in strings) completes
the demonstration of the textual language grammar.

Reply via email to