gracelang / language

Design of the Grace language and its libraries
GNU General Public License v2.0
6 stars 1 forks source link

flattening doesn't work #71

Closed kjx closed 8 years ago

kjx commented 8 years ago

Flattening doesn't work

I've been talking (at NOOL and elsewhere) that the "new" inheritance design is (conceptually) based on flattening. Unfortunately, it can't be, because it at least two case, flattening doesn't work. This means at least that we shouldn't write the spec for inheritance in terms of flatenning.

What is Flattening

The TOPLAS Traits paper says that in any class defined using traits, the traits can be inlined to give an equivalent class definition that does not use traits. So e.g. in the following definitions:

trait aTrait {
  method t { ... body of t defined in aTrait } 
} 
class aClass {
  use aTrait
  method u { ...  body of t defined in aClass }
}

the trait can be flattened away to give

class aClass {
  method t { ... body of t defined in aTrait } 
  method u { ...  body of t defined in aClass }
}

The general idea of flattening I think goes back to Eiffel and probably CLOS; the TOPLAS paper has a formal definition of the flattening property. Unfortunately the current design breaks flattening in at least two ways.

flattening state over initialisation, and aliasing

Flattening works for methods but not for variables, especially where we must consider initialisation and aliasing. Now our current design doesn't put state in traits but it does put state in classes. But if flattening doesn't work for classes, then we either need two descriptions for inheritance and trait composition, or a single definition that doesn't depend on flattening. Here's an example:

class superClass {
 def d = ...some initialisation code... 
} 
class aClass {
 inherit superClass
   alias e = d
}

flattens to

class aClass {
 def d = ...some initialisation code... 
 def e = ...some initialisation code... 
}

where the initialisation code is now run twice. If you naïvely flatten a var the situation is even worse: you'll get two separate vars. What this means is that the conceptual model (and semantics) must introduce hidden undesignatable state "fields" for both vars and defs, along with methods to read (or write) those undesignatable state fields, flatten everything, and only alias/rename/exclude the methods (not the undesignatable fields). Debuggers and reflect presumably need to give access to those undesignatable fields too, somehow...

I think that adopting a shallow "aliasAndExclude" (aka "rename") combinator rather than "alias" combinator solves this, but I'm not sure. Alas there is a worse problem

flattening and implicit sends

Turns out that implicit sends have to be disambiguated in their original context. Consider

  trait t  {
     method a { ....b; c... } 
     method b { ...b defined in trait t ... } 
  }
  method b { ... b defined at module level ... } 
  method c { ... c defined at module level ... } 

 trait u {  
   use t
      exclude b 
    method c { ... c defined in trait u ... }
 }

in the method a defined in trait t, the implciit request to "b" will call the method "b' in the trait, the implciit request "c" will call the method c defined at the module level. But what about trait u? If we flatten like this"

  trait u  {
    method a { ....b; c... } 
    method c { ... c defined in trait u ... }
  }
  method b { ... b defined at module level ... } 
  method c { ... c defined at module level ... } 

the it seems that the implicit "b" request now resolves to the outer scope (where else can it go, given that the b method on the self has GONE) while the implicit "c" request should go to the method on the trait instance. Replacing exclude methods by abstract methods solves the first part of the problem but not the second. oops.

I've always said naïve flattening only works when the context is the same. Now I'm not even so sure about that!

KimBruce commented 8 years ago

Yup, it doesn't work.