Generics?

Generics are an extremely useful part of modern object oriented programming. They allows us to create 'generic' data structures and work with data in a 'generic' fashion, that is to say, they allow us to write code (only once) to work with different classes of instance objects in a type safe manner. Let's look at an example from the pre generic programming days:

class Holder(~holds Object)

Above we have defined a Holder class which holds just one value. So that we can hold instance objects of any type of class, we declared the holds field (accessible via an auto generated getter and setter) to be of type Object. Now let's use it:

holder = new Holder("hi there")
got String = holder.getHolds() as String

Because getHolds returns a type Object, we must always cast the return value back to the type which we stored in the Holder. Note that we must remember what the type is, this is not automatic, this code is not type safe. The following would be perfectly valid code at compile time, but would throw a case exception at runtime:

holder = new Holder("hi there")

holder.setHolds(new Integer(123))//this is ok, Integer is a subtype of Object

got String = holder.getHolds() as String //this will throw a cast exception at runtime because the held value is an integer and not a String thanks to the previous setter call

Surely, there is a better way to do this. Enter generic programming which provides a typesafe mechanism to achieve this.

First we redefine our Holder class with a generic parameter X which is used in place of all instances of the type of our generic parameter:

class Holder<X>(holds X){
  def getHolds() X {
    return holds
  }
	
  def setHolds(newHolds X) void {
    this.holds = newHolds
  }
}

When defining generic types the diamonds <> are used and a comma separated list of generic type parameters are specified.

When specifying generic types it has become customary to use a single uppercase letter to denote the generic type. Not that this is not a requirement, the name used can any valid identifier - but watch out for clashes.

We shall now use the holder in a type safe manner:

holder = new Holder<String>("hi there")

holder.setHolds(123)//this now results in a compile time error!

held String = holder.getHolds() //no cast required

holder.setHolds("new value") //this is ok

Above we can see that we are creating a generic instance of Holder with the new operator by qualifying the generic parameters as a comma separated list inside a diamond pair <>.

We can use any object type and even primitive types as qualifiers for generic types. For instance the following are all valid variable declarations of the generic type Holder:

class Holder<X>(holds X)

inst1 Holder<String>
inst2 Holder<Object>
inst3 Holder<Integer>
inst4 Holder<int>
inst5 Holder<java.util.ArrayList<String>>

If a class has generic parameters, these must be defined when creating instance objects of them (unless the qualification is inferable, see below) and when referring the type. So for example, the below is not valid:

class Holder2<X>{
  ~holds X
}

aholder Holder2 //invalid as we are missing a generic type qualification
another = new Holder2()//also invalid as the generic type qualification is missing and it's not infereable from the constructor (since there are no arguments referencing the generic type)

Generic qualifications for super classes?

Where a super class specifies generic types these can be qualified by using the diamond symbols <> with either another generic type or a concrete type. For example, the following are all valid:

open class SuperClass<X, Y>

class Child1 < SuperClass<String, Integer>//supertype qualified with concrete types

class Child2<A, B, C> < SuperClass<String, Integer>//generic types defined for subclass, unused as far as the superclass qualification is concerned

class Child3<A> < SuperClass<A, Integer>//we use a generic type from the definition of the sub class.

class Child4<A, B> < SuperClass<B, A>//all superclass generic parameters are taken from the subclass

class Child5<A, B> < SuperClass<java.util.HashSet<B>, java.util.HashMap<B, A>>//Here we are using the generic classes from the subclass within concrete classes qualifying the superclass generic types

typedef MyMap<X> = java.util.HashMap<String, X>
class Child6<A, B> < SuperClass<int, MyMap<A>>//now we are referring to a typedef. This code is equivalent to writing: class Child6<A, B> < SuperClass<int, java.util.HashMap<String, A>>

Inference of generic types?

Concurnas is able to infer the generic type qualification of a generic type parameter in a generic class via two mechanisms. Constructor argument qualification and Via usage qualification:

Constructor argument qualification?

The generic types of an instance of a generic class type can be inferred provided that they are referenced in a constructor creating an instance of the generic class type by examining the type of the arguments passed to that constructor. For example:

class Holder<X>(~holds X)

inferred = new Holder("hi there")

Above, inferred is inferred as being of type Holder<String>.

In the case where there are multiple potential types which could quality the generic type, the most specific one is used. For example:

class PairHolder<X>(a X, b X)

infered = new PairHolder(12, 56.22)

Above, inferred is inferred as being of type PairHolder<Number>, as the two arguments satisfying X are both subtypes of type Number.

Via usage qualification?

Concurnas is able to infer generic types based on their localized usage, post construction in a local variable, a top level variable or class field. This is a really powerful mechanism and saves us lots of time when writing code which uses generics, especially when we are not initially calling a constructor which Concurnas can use as par above ("Constructor argument qualification"), in order to infer the constructed object's generic qualification(s). For a variable or field that is missing generic qualification(s) Concurnas is able to infer those generics based on (amongst other things) what methods are called on the variable, what method references are created based on the variable, what values it is later assigned, methods it is passed to as an argument, what generic fields are directly set etc. Here is an example:

class Holder<X>{
  ~holds X
}

holder = new Holder()//holder instance is inferred to be of type Holder<String> based on usesage below
holder.holds("hi there")

Above we see that the holder variable, when declared, it omitting its single genetic type qualification of X. Concurnas is able to infer the type of this though via the usage of holder - the holds's method method is called upon it and this information is used to qualify the generic parameter X to String.

This is an especially useful feature which allows us to define and use some of the Concurnas built in (and auto included) functions as follows:

typedef set = java.util.HashSet

myset = set()//based on the usage below myset is inferred to be java.util.HashSet<Integer>

myset.add(1)
myset.add(2)
myset.add(3)

We see above that myset is inferred to be of generic type java.util.HashSet<Integer> based on the fact that calls to the add method pass in a value of (boxed object) type Integer which qualifies the java.util.HashSet's single declared genetic type.

Partial inference?

Concurnas is able to infer the full generic type qualification for multiple generic types in the case where one or more is qualified via a constructor, and one of more is qualified via usage like so:

class Both<X, Y>(x X){
  def takesOne(b Y){}
}

both = new Both(12)//based on constructor and usage below Both<X, Y> is inferrred to: Both<Integer, String>
both.takesOne("ok")

Above we see that the generic qualifiers for the Both instance is inferable from the constructor call and subsequent usage of the object bot to be: Integer, String

Generics for Methods?

So far we have seen generics being used in the context of classes to make generic classes. But we can also make generic methods and functions. Here are some examples:

class Holder<X>(~held X)

def extractFromHolder<X>(holder Holder<X>) X {
  return holder.getHeld()
}

def swap<X>(h1 Holder<X>, h2 Holder<X>) void {
  tmp = h1.held
  h1.held = h2.held
  h2.held = tmp
}

def duplicator<X>(todup X) X[]{
  return [todup todup]
}

We can call the above functions in just the same way as we ordinarily invoke functions:

res = duplicator<String>("x")//we must qulify the generic parameter type
//res => ["x" "x"]

Inference of Generics for Methods?

Concurnas is able to infer the generic types at invocation point for a method having generic types defined. Carrying on from our previous example:

res1 = duplicator(12) //allow Concurnas to infer the generic type
//res => [12 12]

Nested methods/functions may also have generic types.

Generic lambdas?

Generic Lambdas may be declared and used in the following manner:

lam = def <X>(inst X) X[] { [inst inst]}
res = lam(12)
//res == [12 12]

Concurnas is able to infer the generic types based upon usage of any objects returned from an object having otherwise unqualified local generic types provided that those types are referenced in the returned object:

def meMap<X, Y>() => new java.util.HashMap<X, Y>()

counter = meMap()//created without local generics
counter[0] = "val"

Above, the invocation of meMap can have its local generic types inferred as <Integer, String> based on the usage of the returned type of java.util.HashMap<X, Y> assigned to variable counter.

Generic Method References?

Method references may be specified with generic type qualifications in a relatively unobtrusive manner. Where generic types are involved in method references they can be either qualified at reference creation time, or at invocation time:

def doubler<X>(x X) => [x x]

qualified = doubler<int>&
differed = doubler&

res1 = qualified(12)
res2 = differed('hi')//generic type qualification is infered
}
//res1 => [12 12]
//res2 => ['hi' 'hi']

Generic type restrictions?

The following code is not valid:

class Holder<X>(~holds X)

myHolder Holder<Object> = new Holder<String>("something") //this is not valid code

When it comes to generic type qualification, the types must match on a 1:1 basis. Hence only the following is valid:

class Holder<X>(~holds X)

myHolder Holder<String> = new Holder<String>("something")

In Concurnas, as with most other languages which run on the JVM, generic types are by default, erased at runtime. That is to say that they are not available for reference at runtime. It is for this reason that the following code is not valid:

class Holder<X>(~holds X)

inst Object = new Holder<String>("")

check = inst is Holder<String>//we cannot check the generic qualification

This also means that we cannot overload methods with a type signature differing in generic type qualification only:

def aFunction(op1 java.util.ArrayList<String>){
  //do something
}

def aFunction(op1 java.util.ArrayList<Integer>){//this won't work!
  //do something else
}

The above will fail as the two functions are treated as: def aFunction(op1 java.util.ArrayList) {} def aFunction(op1 java.util.ArrayList) {} which is the same.

However, none of these disadvantages are major and there are some workarounds which we can use to make generic types even more useful. We shall explore some of these next...

Bounded generics?

Concurnas permits generic type definitions to be bounded such that all qualifications of generic types must be equal to or a subtype of the specified bound. This applies to both class level and method level generic definitions. In fact all generic type definitions in Concurnas are bounded implicitly to Object by default.

This is a useful technique for cases where one wants to have some additional control over the types which are applicable for use within a class or method using a generic type. When defining a generic type one we need to add the type after the parameter name, For example:

class MyClass<T Number>(t T){
  def doPlus(arg T) => t.doubleValue() + arg.doubleValue()
}

mc = new MyClass(12.)
result = mc.doPLus(13)

//result == 25.

The above code is possible even though the T type is generic since it is bounded by Number, thus we know that instances of T must be a subtype of Number.

In out Generics?

Generic types on the JVM are invariant, that is to say, list<Object> is not a supertype of list<String>. This can be a problem if we want to write code like the following:

strList = list<String>()
objList list<Object> = strList//this is not permitted

We cannot assign a variable declared as type list<Object> a value of type list<String>1 as it would cause us problems later on when using said lists:

objs.add(1) //totally acceptable, as Integer is a subtype of Object
s = strList.get(0) //This now fails with a ClassCastException!

Above we are adding an Integer into an Object list which is actually a String list. When we come to obtain said value from the list, when it's referenced as a String list we end up with a ClassCastException as the previously added Integer is in our String list (and we expect there to be only objects of type String).

We can largely solve this problem though the use of in/out generics. These provide for a facility known as co- (out) and contra- (in) variance. A neat way to think about this is that in generic parameters may only be consumed by an object (used as inputs to methods), whereas out generic parameters may only be produced by an object (used in return values from methods).

Use-site variance?

Concurnas supports use site variance, this enables us to qualify individual generic parameters when declaring variables of generic types as being either in or out. This enables us to write code such as the following:

covariance in:?

strList = list<String>()
objList list<out Object> = strList//objList may not have anyting added to it

contravariance out:?

strList list<in String> = list<String>()//strList may only have elements added to it 
objList list<out Object> = strList

And so via both these methods we are able to assign our strList to our objList. We could use this technique to say pass a list<String> instance object to a method expecting a list<out Object> type. Note that this all comes at the price of limiting what we can do with our generic objects.

Wildcards?

As we have already seen, in Concurnas, where classes or methods require generic types, these must be qualified (or at least be inferable at compile time). However, sometimes if we want to write code which is agnostic to the type of the generic parameter is then using ? to qualify the type will allow us do this:

from java.util import List, ArrayList

def stringMaker(anyItems List<?>) String {
  ret = ""
  for( x Object in anyItems){
    ret += "" + x
  }
  ret
}

Footnotes

1Of course we can use naughty code like: objList list<Object> = strList as Object as list<Object> to achieve this but it would be inadvisable to do so.