[E-Lang] down with 'define'

Ralph Hartley hartley@aic.nrl.navy.mil
Tue, 06 Mar 2001 10:25:45 -0500


Mark S. Miller wrote:

> At 10:40 AM Monday 3/5/01, Ralph Hartley wrote:
> 
>> Achhh! I **hate** forward declarations!
>> 
>> To be more precise, I hate languages where one might **need** forward 
>> declarations. That the legality, or meaning, of a program should depend on 
>> the order in which definitions appear is simply not palatable.
> 
> Could you give an example of a language that does as you suggest?

I think there are "pure" declarative languages that are religious about 
it, but I don't remember using any.

Certainly lisp (at least traditional dialects) has no need for forward 
declarations, and it is an expression based language (like E). Of 
course, those dialects don't have any type checking to speak of either.

> I just 
> checked Java, and it accepts
> 
>         int i = 0;
>         i++;
> 
> but rejects
> 
>         i++;
>         int i = 0;

Alas, Java is inconsistent on this point. In Java order matters for 
variables, but not for methods. Variables, as you say, have scope 
starting at the declaration. But

class foo {
  void A() {
   B();
  }
  void B() {}
}

is legal.

Top level classes are required to be in separate files, so ordering 
between them is not defined, but classes local to another class don't 
care about order.

> 
>> In any reasonable language, names have a scope. Often this is bounded by 
>> some sort of block (brackets etc.). It really should always be that way, it 
>> should not be bounded by the location of the definition.
> 
> In at least the C like languages of which I'm aware, the rule seems to 
> approximate "name is in scope from point of introduction until the end of 
> block (i.e., close curly)".   That's why a Java programmer would not be 
> surprised at the above result, and would be surprised if the second sequence 
> was accepted and meant the same thing.

The reason java cares about order for variables, is that declaration is 
conflated with initialization. In Java variables are initialized where 
they are declared, and all variables must be initialized ("int i;" 
contains a default initialization to 0). Your second example above is 
not so much an undefined variable, as an uninitialized one (though the 
compiler may claim otherwise).

Variables are an exception in java to the rule that the scope starts at 
the "{".

> Order is space, not time, and is about scoping, not sequence of execution.  
> Definitions are statements about *names*, and names (other than those 
> reserved by the language) must always be local to a scope.  Therefore these 
> statements about names must have a meaning dependent of where the statement 
> occurs.
> 
> Surely we aren't actually arguing about universal vs scoped statements.  I 
> believe we are instead arguing about something like whether the applicable 
> scope starts at the point of introduction or at the previous open curly.

Correct.

> 
>> When I see order sensitive definitions, I think that I am just seeing 
>> assignments disguised as definitions, not **real** definitions.
> 
> 
> Would you apply that to the Java example above?

The initializations in the example ARE assignments, so they do what you 
expect. Java forbids reference to uninitialized variables, so even if 
you extended the scope to the beginning of the block you would be 
limited in what you could do with it. Even writing to it is 
problematical because java needs to know at compile time which 
assignment is the initialization, otherwise it can't be sure there is 
one. So java just doesn't let you do anything with it before the 
initialization (which is always at the declaration).

Java gets away with it (with grumbling), because it is forbidden for 
variables to refer other variables in their declarations (except 
"static" variables, which are more or less constants). So there is no 
dependence in the order between them. You can fix ordering problems by 
just moving them all to the top.

Methods and classes are often intricately interdependent, so a "scope 
starts at declaration rule" for them would cause a problem. That's one 
of the reasons I abandoned C.

>> Also, the only mechanism for using definitions in **different** files, that 
>> I found in Walnut (emakers), is rather heavyweight. Is that the only way?
> 
> I'm glad you raised this.  Currently it is the only way, and it's too 
> painful and urgently needs improvement.  There seems to be a whole subfield 
> of language design on the design of "module" systems, but I am currently a 
> lightweight in this literature.  The issues to be taken into account are at 
> least the usual ones: separate compilation, principled scoping, ease of use.

Without it no one will even consider E for any but the smallest 
projects. This is far more of a "Check box" item than inheritance, and 
with good reason.

> 
>> Of course, I know that for E it is almost certainly way too late for the 
>> sort of basic redesign that would be needed to fix this properly. I guess 
>> I'll just have to wait for "F".
> 
Or maybe not, see below.

> It's very possible.  Y'all have just seen me reject other suggestions on 
> these grounds.  But I think the issues you raised are worth exploring until 
> we at least get a feel for whether the required changes would be too 
> painful.  If you like, I can't reject it until I know what it is. ;)  
> 
> Sometimes the universe gives us happy surprises:  Sometimes conceptually 
> large changes are simply to understand and easy implement, as when I 
> implemented Dean's "var" suggestion in a hour or so.

Even though functions are (in  lisp and E) variables, they don't need to 
be evaluated when a function that uses them is defined. If they don't 
need to be evaluated then, they don't really need to be defined then either.

Now, if you want the signatures of functions (i.e. the types of 
variables) to be checked for consistency at compile time (and of course 
you do), you can't just compile straight through. The compilation of a 
function can't really be complete until all the things it depends on are 
filled in at least enough to know their types.

So whenever your current compiler would complain something is 
"undefined", instead use a placeholder (of the correct type) for the 
using object. That object can still be used, but its compilation can't 
be finished until the things it depends on are defined.

One way to do this would be by a transformation (example not in E syntax):

def type foo ... use of bar ...
def type bar ... use of foo ...

could be replaced by:

forward-def type foo
def type bar ... use of foo ...
def-after-forward foo ... use of bar ...

Since you can always detect when this is needed, and the forward-def and 
def-after-forward parts can all be put at the beginning and end of the 
block, this transformation is easy. It would even be correct to do it 
for every definition.

I think, given the "def" part, you already know how to write forward-def 
and def-after-forward. Making the transformation automatic would remove 
the need for any "sugar". The section in Walnut about this could just go 
away.

I don't know enough to know what else this could collide with in E. I 
don't see there being much, because the code that gets transformed would 
have been illegal before.

Is definition in E really assignment? That is is

startofblock
def foo fistdef
... uses foo ... (firstdef is used)
def foo seconddef
... uses foo ... (seconddef is used)
endofblock

legal? If so, then scope doesn't even really run to the end of the 
block, and we would have a problem (what if both definitions depend on 
bar which is not defined until later). In Java and many other languages 
this would NOT be legal.

Ralph Hartley