Using generic types

Simple types are good for many purposes, but they are poor at modelling collection types such as arrays, maps and sets. Consider arrays as an example. An array of integers seems to be a different type from an array of strings, but they have many things in common.

Generic types allow types to have parameters that modify the base type. For example, the class Array<T> has a single type parameter T which describes the type of the array items. Array<Int> is an array of integers, while Array<Str> is an array of strings.

This section discusses using generic types. We discuss defining generic types and generic functions later in Sections Defining generic types and functions and Generic inheritance and generic interfaces.

Using Array types

The following two functions illustrate using different array types. They simply return the first item of the argument array:

def FirstInt(ai as Array<Int>) as Int
  return ai[0]
end

def FirstStr(as as Array<Str>) as Str
  return as[0]
end

The type of the first return value, ai[0] is Int, since ai is an array of integers. Correspondingly the type of the second return value, as[0], is Str.

Both of the array types in the previous example derive from the same generic type Array<T>; here T is a type variable that can be replaced with any type to create a type instance. All type instances have the same members, but any type variables in the types of the members are replaced with the type parameters of the type instance. For example, the return value of the Array [] operator is declared to be T (remember that the _get method implements the [] operator behind the scenes; the return type of _get is T).

Constructing Array instances

The example above did not create any arrays. Creating an array object with a specific item type is done using an expression of form [...] as <...>. The item type is within angle brackets < > while the array items are within the square brackets:

var a as Array<Int>
a = [1, 3, 5] as <Int>    -- Create integer array with 3 items
a[1]                      -- 3

Type inference for arrays

You can often leave out the as <...> portion when creating arrays. In this case the array item type is inferred:

var a as Array<Int>
a = [1, 3, 5]     -- Ok
a = []            -- Ok

The most common case where you have to give the item type is when you initialize a local variable with an empty array:

def F() as void
  var a = []      -- Cannot infer type of a
  a.append(1)
end

You can correct the code by giving array item type explicitly:

def F() as void
  var a = [] as <Int>       -- Ok, infer Array<Int> as the type of a
  a.append(1)
end

You can also define the type of a explicitly. In this case it is not necessary to give the array item type when creating the empty array:

def F() as void
  var a = [] as Array<Int>  -- Ok
  a.append(1)
end

Note that in the first corrected example above the as <Int> construct applied to the preceding [] expression only; the local variable type is inferred based on the type of the initializer. In the second example, the as Array<Int> construct refers to the variable declaration as a whole. It is equivalent to the following:

def F() as void
  var a as Array<Int>
  a = []
  a.append(1)
end

Multiple assignment with arrays

You can an array value as the rvalue in multiple assignment as in dynamically-typed programs:

def F() as void
  var a = [1, 2]
  var b as Int
  var c as Int
  b, c = a        -- Ok
end

Multiple initialization is also possible:

def F(a as Array<Int>)
  var b, c = a    -- Infer b and c to integers
end
F([1, 2]

We continue the discussion on multiple initialization later in Section Tuple types.

Using Map objects

Map is a type with two generic type parameters, which is expressed by the full type name Map<KT, VT>. Here KT refers to key type and VT refers to value type. Using Map objects is straightforward in statically-typed code:

def F(m as Map<Str, Int>)
  m['foo'] = 5
  m['bar'] = 3
  Print(m['foo'] + m['bar'])
end

Constructing Map object is similar to arrays, but there are two type arguments, and the arguments are given after the call to Map using as <...>:

var m as Map<Str, Int>
m = Map() as <Str, Int>

Type inference is supported for type arguments:

var m = Map<Str, Int>
m = Map()      -- Ok

def F() as void
  var m = Map(5 : 'foo', 3 : 'bar')  -- Infer type to be Map<Int, Str>
end

Pair objects

The Pair type is also a generic type, Pair<L, R>. Usually the type of a pair expression x : y can be inferred, for example:

def F() as void
  var p = 1 : 'foo'         -- Pair<Int, Str>
  p.left                    -- Int
  p.right                   -- Str
end

Sometimes it is necessary to specify type arguments explicitly. Do this using the as <...> construct, as for other generic types. In this case the Pair expression must be within parentheses due to operator precedence:

(nil : 'x') as <Str, Str>     -- Pair<Str, Str>

Generic type compatibility

Two instances of a generic type are compatible, if their arguments are identical. For example, the following code is valid, as it assigns a value of type Array<Int> into a variable of type Array<Int>:

var a = [1, 2, 3] as Array<Int>
var b as Array<Int>
b = a          -- Ok

As always, assignment only copies references to objects, not objects itself. In the above example a and b will refer to the same Array object.

The type checker generates an error if the type arguments are different:

var a as Array<Int>
var b as Array<Object>
a = [1]
b = a     -- Error: incompatible types

As an exception, if the type arguments include type dynamic within them, the types may not have to be identical. You can also sometimes work around the above restrictions by using dynamic casts. Section The dynamic type and mixed typing discusses these topics.

Types which have a subtyping relationship (also generic ones) are also compatible. See Generic inheritance and generic interfaces for a discussion on this.

Nested generics

In the examples above, the type arguments of generic types were always simple types. Type arguments with more complex types are possible and often useful:

var a as Array<Array<Float>>   -- Array of arrays
var m as Map<Str, Array<Int>>  -- Map from Str to Array of Int

You can use an array of arrays (such as a above) to represent a multidimensional array:

a = [[1.0, 0.0, 0.0],
     [0.0, 1.0, 0.0],
     [0.0, 0.0, 1.0]]

Type inference supports complex types, as shown in the example above. Actually, type inference is especially useful when dealing with complex types, as otherwise programming can become very cumbersome. Compare the above fragment to an equivalent one with explicit types:

a = [[1.0, 0.0, 0.0] as <Float>,
     [0.0, 1.0, 0.0] as <Float>,
     [0.0, 0.0, 1.0] as <Float>] as <Array<Float>>

Constructing arbitrary generic instances

You may have figured out the pattern for constructing generic instances with explicit type arguments. You can construct an instance of arbitrary generic type X<T1, T2, ...> like this:

X(...) as <A1, A2, ...>

The above form is called a type application. The first ... refers to the arguments to the create method; they are ordinary values, not types. A1, A2, ... are types; they are the values of the type arguments T1, T2, etc.

For example, constructing an instance of Set<T> with explicit type arguments happens like this:

import set

Set([1, 2, 6, 3]) as <Int>

As usual, the type application above is unnecessary as the type checker can infer the type argument.