#29 Parameterized Types

brian Fri 13 Jan 2006

As you guys know, I strongly believe user defined generics create way too much complexity to justify the problem they solve. However I do want to support language defined generics for the List and Map class. I thought long and hard about if/how user defined classes might subclass Lists and use parameterized types, but I decided that didn't make sense given the complexity introduced. I think just using List and Map as-is via delegation is probably the right compromise. For example instead of subclassing Employee[] you would just provide a method that returned Employee[]. This means List will play an extremely important role since it will be heavily used in a wide variety of circumstances, and will not be extendable (at least not via subclassing or with parameterized types).

My design proposal is to define two special types - sys::V and sys::K which are respectively the parameterized value type for List/Map and the parameterized key type for Map. These predefined parameterized types will be used to specify the signatures for List and Map, for example:

class List
{
  V get(Int index)
  List add(V val)
}

class Map
{
  V get(K key)
  V put(K key, V val)
}

The compiler will treat these specially - there won't actually be a source code definition for V or K. In a List type such as Foo[], all signatures with V are replaced with Foo. With a Map such as Foo[Bar], V is replaced with Foo and K with Bar.

The V and K types will continue to be used in fcode. For example the signatures above in the fcode will contain sys::V and sys::K. Like the source code, there will not be a fcode type definition for V or K.

At runtime the emitter won't do anything special for lists or maps. With the exception that a automatic narrowing cast will be inserted on methods that return V or K (to satisfy the verifier). No casts will be inserted for method parameters. Types like Str[] will just be a List, however I will pass around a Type as a field inside the List for handling instanceof.

Regarding reflection, no access to the parameterized types will be available. Rather return types and parameter types will just be whatever the actual type is:

Obj[].type.method("get").returnType   -> Obj
Str[].type.method("get").returnType   -> Str
Int[][].type.method("get").returnType -> Int

Let me know if you have any questions or comments, otherwise I am going to implement as proposed (hopefully this weekend). Just about everything I've done since before x-mas was to get me to a point where I could solve this problem (which I roadblocked on when working on List way back when).

brian Sat 14 Jan 2006

I got the first half of this working tonight and checked in. The List methods in Fan source now have a V signature and are compiled correctly into fcode.

I also got the runtime to begin to understand parameterized types. So consider these signatures in Fan:

class List
{ 
  V get(Int index)
  List add(V item)
}

Now when you reflect a specified List, V and List get substituted:

Str[].type.method("get").returnType -> Str
Str[].type.method("add").returnType -> Str[]
Str[].type.method("add").params[0]  -> Str

Now I need to take a detour to come up with an efficient way to implement the LoadType opcode so that I can do fast List/Map parameterization (I also need it for the is/as operators).

One big question I have for you guys is how to apply the standard naming conventions to a List? For example:

Type t = acme::Foo[].type
t.qname    -> acme::Foo[]?
t.pod.name -> acme?
t.name     -> Foo[]?

That sort of makes sense, but then again t is really sys::List so it doesn't make a whole of sense to say that a parameterization of List is scoped by the acme pod. If it does make sense to you, then consider Map which has two parameterized types:

Type t = acme1::Val[acme2::Key]
t.qname    -> ?
t.pod.name -> ?
t.name     -> ?

andy Sat 14 Jan 2006

I think it makes to say that the type of Str[] is Str[] and still always check for List with (t is List), so:

Str[] f = ["a", "b", "c"]
a = (f is Str[]) // return true
b = (f is List)  // return true
c = (f.type.name == "Str[]") // return true

Same goes for Map

Int:Str f = [0:"a", 1:"b", 2:"c"] // syntax?
a = (f is Int:Str) // return true
b = (f is Map)     // return true
c = (f.type.name == "Int:Str") // return true

brian Sun 15 Jan 2006

Ok, I checked in the rest of the code to implement parameterized types. The runtime half of the design is not as clean as the parameterized signatures - mostly because I don't understand how they will be implemented in .NET. I suspect we will want to use .NET's support for generics, which may cause us to rethink the design. But John is a ways away from getting to that point, so I choose a Java-like erasure design for now.

When calling a parameterized method, the call signature just uses sys::Obj for anything parameterized as V or K (since this is the actual signatures in the implementation). However since return types are narrowed, I insert a Cast operation into the fcode. To summarize: the call signatures don't know anything about parameterized types, and I'm handling the return in the assembler rather than in the emitter. We will need to revisit when John gets to this in .NET.

To illustrate:

Str f(Str[] x) { x.add("a"); return x[0] }

Compiles to:

sys::Str f(sys::List x) [public]
  [Code]
    0: LoadVar       1
    3: LoadStr       a
    6: CallInstance  sys::List.add(sys::Obj) -> sys::List
    9: Pop
   10: LoadVar       1
   13: LoadInt       0
   16: CallInstance  sys::List.slice(sys::Int) -> sys::Obj
   19: Cast          sys::Str
   22: ReturnObj

Login or Signup to reply.