[E-Lang] Syntax change: reducing side-effects
Mark S. Miller
markm@caplet.com
Sun, 11 Feb 2001 23:27:22 -0800
Terminology: Until we decide whether or not to incorporate "var" into E, in
order to keep things straight, let's call Dean's proposed variant of E
"Evar". I have a working (unreleased) Evar, which I used in the examples
below.
At 04:47 PM Sunday 2/11/01, hal@finney.org wrote:
>With the proposed convention I gather I'd have to add the "var"
>declaration to all my function names. Then when I had them all working
>the way I wanted I'd have to go through and take all the "var"s out.
>
>I've always liked interactive development systems that let you work like
>this, going back to APL. You define a function and then try calling
>it from the command line, and when it works you define more complex
>functions that use it. Does anyone else use E this way?
Hal, I'm really glad you raised this, because in all the discussions so far
between Dean, MarcS and I, none of us ever noticed the issue. E is indeed
designed to support this kind of interactive shell/command-line usage,
including the redefinition of previously defined functions/objects. In
fact, I'll go ahead and call this a "requirement" since I'd be really
unhappy if we lost this functionality. When I first read your message I was
worried.
Then I realized all is well. Your example isn't assigning to these
variables, it's redefining them. In general E does not allow redefinition
of the same variable name in the same scope. The top level interactive
scope is the one exception to this
http://www.erights.org/javadoc/org/erights/e/elang/scope/MutableScope.html
, in order to support exactly the style of interactive development / rapid
prototyping you're using. Since the "var" change only affects assignment
and not definition, this special interaction between redefinition and the
top level interactive scope should be unaffected. Here's an Elmer on Evar
session demonstrating these points. I removed the stack backtrace from the
problem reports below.
For background, see http://www.erights.org/javadoc/org/erights/e/elib/slot/ValueGuard.html and http://www.erights.org/javadoc/org/erights/e/elib/slot/SlotGuard.html .
? {
> def a := 3
> def b := 4
> def a := 5
> }
# problem: <AlreadyDefinedException: conflicting definition of a>
This demonstrates that a non-top-level scope does not allow a variable to be
redefined.
? def x :integer := 33
# value: 33
This "x :integer" is equivalent to the E "x :final(integer)"
or the Evar "var x :final(integer)".
? x := 44
# problem: <ClassCastException: org.erights.e.elib.slot.FinalSlot>
We should have a more informative error message for assignment to a final
variable. Actually, E and Evar specify that a program with such an
assignment must be statically rejected, and not allowed to begin execution.
Postponing the error check till runtime is an old known bug. In E, it was
never high priority to fix this bug, because most variables were implicitly
mutable anyway. In Evar, this static check should be much more useful.
? def func() { println(x) }
# value: <func>
? func()
33
This function definition uses "x" freely, thereby capturing "x" as an
instance variable.
? var x :char := 'a'
# value: a
In answer to Hal's worry, this redefinition succeeded even though the above
assignment failed. By being defined as final, x could not be reassigned,
no matter what scope it's defined in. By being defined in the top level
interactive scope, x can be redefined, independent of what it's defined as.
Yet Another 2x2 Matrix: (Scope, Slot) x (Immutable, Mutable)
To understand the semantics of this, let's make explicit the two levels
needed to map a variable name to the value of that variable. For these
purposes, we will refer to the above expression as doing two things:
*defining* the variable to be represented by a Slot, and *initializing* the
Slot with an initial value.
* Evaluation happens in a scope. A scope is a mapping from names to Slots.
A variable definition creates a new name => Slot mapping. A variable name
expression is evaluated in a given scope by first looking up the Slot bound
to that name in that scope.
Scopes are normally immutable, so the Slot for a given name in a given
scope cannot be changed -- normal scopes do not allow redefinition. The
exception is MutableScope, expected to be used only in the top level
interactive environment.
* A Slot responds at least to getValue(). MutableSlots additionally respond
to setValue(newValue). We refer to the result returned by calling
getValue() on a given Slot as "the Slot's value". A Slot's initial value is
expected to be a coercion of the specimen provided to the SlotGuard that
made the Slot (eg, the value of the right side of a "define"), but that's up
to the SlotGuard and the Slot it makes to decide.
To evaluate a variable used as an expression (rValue), the implementation
calls getValue() on the Slot obtained in the first step. Whatever it
returns is the value of the expression. To evaluate an assignment to the
variable (lValue), the implementation calls setValue(newValue) on the Slot,
where newValue is the value of the expression to the right of ":=". Of
course, following an assignment, the Slot's value is expected to be a
coercion of newValue, but that's up to the Slot to decide.
final Slots are immutable, so the value of a final Slot won't change. The
final Slot does not allow assignment. In Evar, Slots bound to variables are
normally final. The exceptions include only user-specified SlotGuards, for
which the "var" syntax is required.
To repeat for context, and to demonstrate that it's harmless in Evar...
? var x :char := 'a'
# value: a
In Evar, "var x :char := 'a' " is shorthand for "define var x :char := 'a' ".
In the expanded form, the "var x :char" is equivalent to the E "x :char".
"char" obeys the normal type-like guard convention: It implements both
ValueGuard and SlotGuard protocol. As a ValueGuard, it only accepts values
it can coerce to the type it represents. As a SlotGuard, it is equivalent
to "settable(char)", which makes a SlotGuard that makes a settable
(assignable) slot that coerces assigned values using "char" as its ValueGuard.
Therefore, *given the behavior of "char"*, "var x :char" is equivalent
to "var x :settable(char)".
? x := 'b'
# value: b
? x := 44
# problem: <ClassCastException: java.math.BigInteger doesn't coerce to a
java.lang.Character>
These demonstrate that x's nature seems completely based on the second
definition and not the first. It is now settable (assignable), whereas
previously it was final. It is now constrained to hold only characters, not
integers, whereas previously the opposite was true. This complete erasure
of the old definition with the new is needed for this kind of interactive
prototyping. However, it raises a potentially nasty question.
The Strange Case
When "func" was defined, we say it captured (as an instance
variable) the variable "x", but what exactly did it capture. A plausible
hypothesis would be that, on creating the object bound to "func", the Slot
for "x" was looked up and that "func" contained it's own private mapping
from "x" to this Slot.
Unfortunately, this story would make Hal's style of prototyping very
tedious. On redefining anything, Hal would need to redefine everything in
its scope. Fortunately, this isn't what E does.
? func()
b
Here we see that "func" has looked up the Slot bound to "x" by the most
recent assignment, rather than the most recent prior to its own definition.
To get this right, "func" actually captures a reference to the outer scope
itself as an instance variable, and actually looks up the names at runtime
(by sending messages to this scope object). There are of course many
opportunities for transparent compiler optimization here, but when the outer
scope is mutable, not as many opportunities as one might expect. OTOH, the
other scope should only be mutable in an interactive context, which is
hardly the most demanding for speed.
Please indulge me while I brag
Needless to say, it was not easy to work out the above Scope vs Slot level
separation, the behavior of the top-level interactive scope, satisfy the
rapid prototyping requirements, and satisfy the compiler requirements for
static analyzability, all within a simple orthogonal semantic story that
most programmer can safely avoid ever learning. Whew, it's even hard to
say. However, this separation is about two years behind me now, and has
worked so perfectly during that time, that I simply forgot about it when
discussing "var" with Dean and MarcS and when building the Evar used above.
Until Hal's message reminded me. The fact that this Evar works anyway gives
me some confidence I got this separation right.
It took Dean, MarcS, and I weeks of occasional argument to arrive at an Evar
proposal worth taking seriously. But once it settled, it took me less than
an hour to implement.
Cheers,
--MarkM