Introduction

Generic types can be used in ABL to write classes and interfaces in which the type parameters are abstracted out. This allows the class to defer the specification of such parameters until the class is declared and instantiated by a client of the class. Using a generic type reduces and simplifies code since you do not have to create a class for each distinct type and you do not have to cast references at runtime, ensuring type safety.

Algorithms and data structures are a good use case for generics because data structures have a type. If the type is extracted out through use of generics, then the code can be greatly simplified. The built-in Progress.Collections classes (List, SortedSet, and HashMap) are examples of such a use case.

See Generics concepts and terminology in object-oriented ABL for more information on the terminology used for generics in ABL. The rest of this topic describes the mechanics of ABL that allow user-defined generic types.

CLASS statement

You use the CLASS statement to define a generic type by specifying the type parameter and type constraint within angle brackets (<>). This is the simplified syntax for the CLASS statement (additions to support generics are shown in bold):

CLASS class-name 
  [ <type-param AS type-constraint [, type-param AS type-constraint]...> ]
  [ INHERITS super-class-name ]
  [ IMPLEMENTS interface-name [ , interface-name ] ... ]
  [ USE-WIDGET-POOL ] [ ABSTRACT | FINAL ] [ SERIALIZABLE ]:  
  class-body
<type-param AS type-constraint [, type-param AS type-constraint]...>
Type-param is a type parameter name for a generic type. Type-param is, by convention, a single uppercase letter. For instance, T (for type), K (for Key) and V (for value) are some commonly used type parameter names.
Type-constraint is a type name by which type-param is constrained, that is, the expected type that can be used as type arguments in the client of the generic type. The compiler enforces that only objects of such type, or inherited from it, can be used as type arguments, and the generic class can be coded knowing that only objects compatible with that type are allowed. For example, given a user-defined class called baseClass, T AS baseClass defines a constraint that to use such generic type, you must specify a type argument that is, or inherits from, baseClass.

The type-param acts as a type alias for the type arguments supplied by the client of the generic type. The type parameter name has the same naming restrictions that apply to class, interface or enum names (for example, alphanumeric, first character must be alpha, and so on). The following is an example of a user-defined generic type:

CLASS SimpleGenericType<T as Progress.Lang.Object>.

The type-constraint must be either an object-oriented ABL interface or class (deriving from Progress.Lang.Object). .NET types are not allowed as arguments on the user of the type. Primitive types (such as INTEGER, CHARACTER, ...) are also not allowed. The following examples show invalid syntax:

CLASS SimpleGenericType<T AS CHARACTER>. //invalid

CLASS SimpleGenericType<T as System.Object>. //invalid

The type-params may be referenced in the same way any type may be referenced inside the class. The following statements are examples of references to the type parameter:

DEFINE PROPERTY someprop AS T GET.

METHOD PUBLIC VOID Insert(idx AS INTEGER, item AS T): ... END. 

METHOD PUBLIC T Get(idx AS INTEGER): ...

Note that like non-generic classes, generic classes must be in a file with the .cls extension and the name of the class in the CLASS statement must match the name of the file. For example, CLASS SimpleGenericType<T as Progress.Lang.Object> must be saved in the file SimpleGenericType.cls. This means the class type names share the same namespace and you cannot have a generic class and non-generic class with the same name. When compiling such classes, you get a single r-code (.r) file for the generic type.

You can have more specific type constraints that limit the types of type arguments a client of the generic type can specify. For instance, you can define a type as a specific class type:

CLASS SimpleGenericType<T as mypackage.myroot>.

In this case, the client of this generic type is allowed to specify a type argument with a type that is compatible with mypackage.myroot. The same rule applies to an interface type defined as the type constraint.

The type-param may be referenced in the INHERITS and IMPLEMENTS phrases as a type argument to a generic type. The following is an example using the built-in Progress.Collection.IComparable interface:

CLASS SimpleGeneric<T AS Progress.Lang.Object> 
  IMPLEMENTS Progress.Collections.IComparable<T>:

  METHOD PUBLIC INT CompareTo(INPUT pObj AS T):      
  END.
END.

Since type constraints must be object-oriented ABL types, they can also be a generic type with type arguments. For example:

CLASS SimpleGeneric<T AS Progress.Lang.List<Progress.Lang.Object>>

In the example above, the client using generic type SimpleGeneric must specify a type argument that is compatible with List<Object>.

Non-generic classes can inherit or implement generic types, and vice versa. For example:

CLASS SimpleClass INHERITS SimpleGeneric<Employee>:

CLASS SimpleGeneric<T AS Progress.Lang.Object> INHERITS myRootClass:

CLASS SimpleClass2 INHERITS SimpleGeneric<SimpleClass2>:

Note in the third example that the class being defined, SimpleClass2, is also being passed as the type argument for the generic class it is inheriting, SimpleGeneric. The argument is the type that is passed during instantiation. This is comparable to having a variable in the class having the type of SimpleClass2:

DEFINE PUBLIC VARIABLE selfRefType AS CLASS SimpleClass2.

The access modifiers (PUBLIC, PROTECTED, and so on) apply to generics as well. Generic types may also be declared to be either ABSTRACT or FINAL, just like any other class type and the existing rules apply.

INTERFACE statement

You use the INTERFACE statement to define a generic type by specifying the type parameter and type constraint within angle brackets (<>). This is the simplified syntax for the INTERFACE statement (additions to support generics are shown in bold):

INTERFACE interface-type-name 
  [ <type-param AS type-constraint [, type-param AS type-constraint]...> ]
  [ INHERITS super-interface-name [, super-interface-name ] ... ] : 
  interface-body
<type-param AS type-constraint [, type-param AS type-constraint]...>
Type-param is a type parameter name for a generic type. Type-param is, by convention, a single uppercase letter. For instance, T (for type), K (for Key) and V (for value) are some commonly used type parameter names.
Type-constraint is a type name by which type-param is constrained, that is, the expected type that can be used as type arguments in the client of the generic type. The compiler enforces that only objects of such type, or inherited from it, can be used as type arguments, and the generic class can be coded knowing that only objects compatible with that type are allowed. For example, given a user-defined class called baseClass, T AS baseClass defines a constraint that to use such generic type, you must specify a type argument that is, or inherits from, baseClass.

The same rules for type parameters and type constraints described for the CLASS statement also apply to the INTERFACE statement.

The following examples show generic interfaces, along with the type parameters and supported inheritance:

INTERFACE IEmployee<T AS Progress.Lang.Object>: 
  METHOD PUBLIC VOID Insert(idx AS INTEGER, item AS T). 

INTERFACE IManager<T AS Progress.Lang.Object> 
  INHERITS IEmployee<T>:

Non-generic interfaces can inherit generic interfaces and vice-versa as shown in the following example:

INTERFACE IManager<T AS Progress.Lang.Object> 
  INHERITS myIface.

INTERFACE OtherIface INHERITS IManager<Manager>.

Type references

ABL supports generic types everywhere an object-oriented ABL reference can be used. This includes variable and property definitions, parameter definitions, return type for methods (in class or interface) or user-defined functions, the NEW expression, the TYPE-OF(), CAST() and GET-CLASS() functions, the USING statement, and other places.

Type arguments cannot be primitive types (for example, INTEGER, CHARACTER), .NET types, or handle-based objects. For example:

VAR SimpleGeneric<int> myGeneric. //compiler error

Overloading

The existing rules for method overloading apply to generic classes.

You cannot define more than one method that takes the type parameter as a parameter. This is similar to any object-oriented class. Therefore, the following code is invalid:

CLASS MyGeneric<T AS Progress.Lang.Object>: 
  METHOD VOID Add(val as T):
    ... 
  END. 

  METHOD VOID Add(val AS T):   //This is a duplicate
    ... 
  END. 
END.

You should be cautious regarding methods where the type parameter is involved. For example, given a generic class like this:

CLASS MyGeneric<T AS Progress.Lang.Object>: 
  METHOD VOID Add(val AS T): //Add #1 
    ... 
  END.

  METHOD VOID Add(val AS Employee): //Add #2 
    ... 
  END.

If the client of that generic type passes Employee as the type argument, then the compiler will generate an ambiguous call error for this piece of code, since there are two Add() methods that could satisfy the method call:

VAR MyGeneric<Employee> myEmployee = NEW MyGeneric<Employee>().

myEmployee.Add(NEW Employee()).

Now consider the following method defined in the above class, where the Add() method is called from within another method in the same class:

METHOD VOID Add2(obj AS Progress.Lang.Object):

  THIS-OBJECT:Add(obj). //resolves to Add #1 
  THIS-OBJECT:Add(new Employee()). //resolves to Add #2

Because T is defined as a constraint on Progress.Lang.Object, the compiler matches obj to T in the first Add() call. The second one can match to the specific type.

CAUTION: When working with generic types, do not have overloaded methods that can conflict with the definition of T by the client of the generic type, to avoid confusion and errors.

Overriding

Everywhere object-oriented ABL permits an element (method, property, event) to be overridden, elements within generic types may also be overridden. When an object-oriented ABL element is overridden, it may only be overridden with the exact matching signature. This is the existing rule for object-oriented ABL objects and is no different for elements which have generic types in their signature.

Object serialization

Generic types can also be defined as SERIALIZABLE and the existing rules for serialization apply, that is, all classes in the hierarchy, and the class types of all data member used in the class (unless defined as NON-SERIALIZABLE) must also be serializable (unless the type is an interface). In addition, the type for the type parameters of a generic class must also be serializable. Otherwise, you get a compile time error when compiling a class with the SERIALIAZABLE option. For example:

CLASS DerivedGeneric<T AS myObjType> 
   INHERITS SimpleGeneric<T> SERIALIZABLE

The compiler raises an error if either SimpleGeneric or myObjType is not marked as SERIALIAZABLE.

Additionally, the type argument of a client of the generic class must also be SERIALIAZABLE for serialization of the object to occur, otherwise you get a runtime error when trying to serialize such object.

Reflection

Reflection on user-defined generic types is supported. See Progress.Lang.Class class which contains the following methods: IsGeneric(), GetTypeParameters(), and GetTypeArguments(),

Events

When an event is defined, a signature is specified to be used for the event. Subscribers may subscribe to their own event handler method, which must match the signature defined for the event. The publisher publishes the event using the same signature. That signature may reference object-oriented ABL types, including generic types.

The following example defines an event in eventClass with a generic parameter myList<custEntity>:

DEFINE PUBLIC EVENT CustomerList
  SIGNATURE VOID (INPUT mlcRef AS myList<custEntity>).

The following example subscribes to the above event and the subscribed method:

METHOD PUBLIC VOID SubCustList (INPUT ecRef AS eventClass):
  ecRef:CustomerList:Subscribe (CustomerList_Handler) NO-ERROR.
END METHOD.

METHOD PUBLIC VOID CustomerList_Handler
 (INPUT mlcRef AS myList<custEntity>):
    rPubObj:NewCustomer:Unsubscribe (CustomerList_Handler) NO-ERROR.
END METHOD.

The compiler does the same type compatibility checks for generic types that it does for non-generic types.

Restrictions

See Restrictions of user-defined generics for information on the restrictions of user-defined generics.

Example

For sample code of user-defined generics, see Example: User-defined generics.