The Beauty of Swift Language – Generics

👍
· Nov 26, 2023 · 12 mins read
The Beauty of Swift Language – Generics

Generic is a very important concept in strongly typed languages. For type safety, without generics, any types and methods can only handle the types they’re designed for. On the other hand, generics allows types or methods to handle more than one types. For example, in Swift, all collection types like Array, Set and Dictionary are generic types. Thus, we can we can use them with many types. For instance, an Int Array and a String Array etc.

Generics are not only used in system types. As programmers, we can also define our own generic types or methods. Using generics correctly usually can improve code reuse, ensure type safety and boost performance. However, it also often requires more effort to write generic types or methods. In this article, we are going to explore how generics can be used in Swift and how it’s different from other languages like C# and Kotlin.

Key Concepts

Type parameter and type argument

In order to express the idea that the type/method can handle more than one types, the languages need to have a syntax to support this. This is where type parameter and type argument come in. When we define a generic type/method, we use type parameter in the definition to indicate that it can handle multiple types and we use type argument to specify the type when we use this generic type/method, which will be shown in the following sections.

Variance

Morphism is a critical concept for objects in the Object Oriented language world. It essentially means some object type instances can behave as other type instances sometimes. For example, a subclass instance can behave as its base class instance sometimes. When this come into play with generics, it could become very interesting. For instance, can we assign a value of a generic type with a subclass as type argument to a variable of the same generic type that uses the base class as type argument? If we can, we can say a type is a subtype of another type, or vice versa.

The variance concept focuses on the relationship of types: whether a type is a subtype of another type. In total, there are three variance types: covariance, contrivance and invariance. Essentially, covariance means the derived type will keep the order of its parent type. For example, if type A is a subtype of B, the derived type A(with A as type argument) will still be a subtype of the derived type B(with B as type argument from the same Generic type). Contrivance means the relationship of the derived types is reversed. Lastly, invariance means there is no relationship between the derive types at all.

Different languages have different syntaxes to express these variance ideas, which will be shown in the following sections. It is worth mentioning that the variance concept is not limited in classes and it subclasses, it’s a concept that applies to all types including protocol/interface types, function types etc.

Type Constraint

To develop the type and subtype concepts in generics further, what if we want to define a generic type/method that can only be used for certain subtypes? In this case we cannot just use a generic type parameter because it means that any types can be used. In order to limit the subtypes that can be used in the generic definition, we need a way to put some type constraints in and different languages have different syntaxes for this as well, which will be explored in the following sections.

Define Generic types

The syntaxes to define Generic Types are very similar in Swift, C# and Kotlin. The Type parameters can be used just like normal Types in the generic type definitions and we use concrete type arguments to replace the type parameters in Generic types, which also means all the generic types instances are concrete types at runtime. Let’s compare how these are done in these languages. The T and U in the examples are type parameters.

Swift

Swift allows to create generic struct, class, actor, protocol, enum types. Let’s use generic class as an example:

// Define  
class GenericClassExample<T> { }     // With one type parameter  
class GenericClassExample2<T, U> { } // With two type parameters  
  
// Use  
let generic1 = GenericClassExample<Int>()  
let generic2 = GenericClassExample2<Int, String>()

C#

In C#, we can define generic struct, class, interface and record types, but not enum type. For generic type definition, the syntax is like below:

// Define  
class GenericClassExample<T> { }     // With one type parameter  
class GenericClassExample2<T, U> { } // With two type parameters  
  
// Use  
var generic1 = new GenericClassExample<int>();  
var generic2 = new GenericClassExample2<int, string>();

Kotlin

We can define generic class, interface in Kotlin. Let’s take generic class definition as an example like below:

// Define  
class GenericClassExample<T> { }     // With one type parameter  
class GenericClassExample2<T, U> { } // With two type parameters  
  
// Use   
val generic1 = GenericClassExample<Int>()  
val generic2 = GenericClassExample2<Int, String>()

Define Generic methods

The methods defined in a generic type can use the type parameter of that type as a normal type. The following will discuss the generic methods that does not use the type parameters of the parent type that they are in.

The syntaxes of defining and using generic methods in these three languages are very similar. Just as we can use the type parameters defined in the Generic types as normal types in the type body, we can use the type parameters defined in the generic method declaration as normal types in method body.

Swift

// Define  
func genericMethod<T>(parameter: T) { } // With one Generic type parameter  
func genericMethod<T, U>(parameter: T, parameter2: U) { } // With one Generic type parameters  
  
// Use  
genericMethod<Int>(parameter: 1)  
genericMethod<Int, String>(parameter: 1, parameter2: "abc")  
  
// Or use it relying on type inference  
genericMethod(parameter: 1)  
genericMethod(parameter: 1, parameter2: "abc")

C#

// Define  
void genericMethod<T>(T parameter) { } // With one Generic type parameter  
void genericMethod<T, U>(T parameter, U Parameter2) { } // With one Generic type parameters  
  
// Use  
genericMethod<int>(1);  
genericMethod<int, String>(1, "abc");  
  
// Or use it relying on type inference  
genericMethod(1);  
genericMethod(1, "abc");

Kotlin

// Define  
fun <T> genericMethod(parameter: T) { } // With one Generic type parameter  
fun <T, U> genericMethod(parameter: T, parameter2: U) { }   // With one Generic type parameters  
  
// Use  
genericMethod<Int>(1)  
genericMethod<Int, String>(1, "abc")  
  
// Or use it relying on type inference  
genericMethod(1)  
genericMethod(1, "abc")

Express Variances

Variances indicate if it’s type safe to do certain operations. As a rule of thumb, if the type is used only on return value, it’s covariant. If the type is used as the type of input parameters, it’s contravariant. Let’s see how this can be done in these languages.

Swift

In Swift, all custom generic types are invariant by default, which means none of the following works.

class Base {}  
class Sub: Base {}  
class GenericClass<T> { }  
  
// let base = GenericClass<Base>()  
// let sub: GenericClass<Sub> = base  
  
// let sub = GenericClass<Sub>()  
// let base: GenericClass<Base> = sub

It’s worth mentioning that the native collection types have special support on this like below:

// However, this works because Swift has special treatment for the generic collection types  
let subArray: [Sub] = []  
let baseArray: [Base] = subArray  
  
// This doesn't work because Array is covairant type  
// let baseArray: [Base] = []  
// let subArray: [Sub] = baseArray

Swift doesn’t provide any syntax to provide variance in the generic definitions at the moment.

C#

Similarly, all custom generic types are invariant in C# by default.

class Base {}  
class Sub: Base {}  
class GenericClass<T> { }  
  
// Cannot assign a value of type GenericClass<Base> to a vairable of type GenericClass<Sub>  
// var baseGeneric = new GenericClass<Base>();  
// GenericClass<Sub> subGeneric = baseGeneric;  
  
// Cannot assign a value of type GenericClass<Sub> to a vairable of type GenericClass<Base>  
// var subGeneric = new GenericClass<Sub>();  
// GenericClass<Base> baseGeneric = subGeneric;

Again, C# supports covariance on its native array types.

// However, this works because C#'s specical treatment of Array type  
Sub[] subArray = [];  
Base[] baseArray = subArray;  
  
// The following does not work because array is covariant type  
// Base[] baseArray = [];  
// Sub[] subArray = baseArray;

However, C# allows to declare variance for Generic Interfaces. To specify covariance, us out keyword like below:

class Base {}  
class Sub: Base {}  
interface ICovariant<out R> { } // out means covariance  
class TestA: ICovariant<Base> { }  
class TestB: ICovariant<Sub> { }  
  
// With covariance, this will work  
ICovariant<Sub> subInterface = new TestB();  
ICovariant<Base> baseInterface = subInterface;  
  
// But this won't work  
// ICovariant<Base> baseInterface = new TestA();  
// ICovariant<Sub> subInterface = baseInterface;

To specify contravariance, us in keyword like below:

class Base {}  
class Sub: Base {}  
interface ICovariant<in R> { } // in means contravariance  
class TestA: ICovariant<Base> { }  
class TestB: ICovariant<Sub> { }  
  
// With contravariance, this will work  
ICovariant<Base> baseInterface = new TestA();  
ICovariant<Sub> subInterface = baseInterface;  
  
// But this won't work  
// ICovariant<Sub> subInterface = new TestB();  
// ICovariant<Base> baseInterface = subInterface;

Kotlin

Likewise, all custom generic types are invariant in Kotlin by default. So none of the following works.

open class Base {}  
class Sub: Base() {}  
class GenericClass<T> { }  
  
// This won't work  
// val base = GenericClass<Base>()  
// val sub: GenericClass<Sub> = base  
  
// This also does not work  
// val sub2 = GenericClass<Sub>()  
// val base2: GenericClass<Base> = sub2

In contrast to C# and Swift, Kotlin doesn’t provide covariance support for array type. So the following won’t work.

// val baseArray: Array<Base> = emptyArray()  
// val subArray: Array<Sub> = baseArray  
      
// val subArray: Array<Sub> = emptyArray()  
// val baseArray: Array<Base> = subArray

If we switch to List, it will work for covariance case like below because Generic List support covairance:

// This will not work because it's not contravairant  
// val baseList: List<Base> = emptyList()  
// val subList: List<Sub> = baseList  
  
// This works  
val subList: List<Sub> = emptyList()  
val baseList: List<Base> = subList

Kotlin uses so called Declaration-site variance for this purpose, which also uses out and in keyword for covariance and contravariance respectively like in C#. However, unlike C#, those keywords in Kotlin can be used with Type parameters in all types of generic definitions.

Use out to specify covariance:

open class Base {}  
class Sub: Base() {}  
class GenericClass<out T> { }  
  
// This works as GenericClass is covairant with Generic Type T  
val subGenericClass = GenericClass<Sub>()  
var baseGenericClass: GenericClass<Base> = subGenericClass  
  
// This will not work  
// val baseGenericClass = GenericClass<Base>()  
// var subGenericClass: GenericClass<Sub> = baseGenericClass

Use in to specify contravariance:

open class Base {}  
class Sub: Base() {}  
class GenericClass<in T> { }  
  
// This works as GenericClass is contravairant with Generic Type T  
val baseGenericClass = GenericClass<Base>()  
var subGenericClass: GenericClass<Sub> = baseGenericClass  
      
// This will not work  
// val subGenericClass = GenericClass<Sub>()  
// var baseGenericClass: GenericClass<Base> = subGenericClass

Add type constraints

Swift

In Swift, there are two ways to set constraints to the Generic type: Generic Parameter Clause and Generic Where Clauses like below:

// Use Generic Parameter Clause only  
class GenericClass<T: Comparable & Hashable>  {  
}   
// Use Generic Parameter Clause only and Generic Where Clause  
class GenericClass<T: Comparable> where T: Hashable {   
}

C#

In C#, use where to add constraints to Type parameters like below and more details can be found here.

class GenericClass<T> where T : System.IComparable<T>, new() { }

Kotlin

Kotlin offers two ways to set constraints to Generic types: Type Upper bounds and where clause.

// The upper bound of T is Comparable<T>  
class GenericClass<T: Comparable<T>> {  
}  
// The upper bound of T is Comparable<T>  
// Meanwhile it has to satisfy with the where clause T : CharSequence  
class GenericClass<T: Comparable<T>> where T : CharSequence {  
}

Conclusion

In this article, we have explored the concept of generics and how we express generic ideas in Swift, C# and Kotlin at a very high level. While Swift offers the most concise and expressive syntax for generic programming, as you might have noticed, it seems lacking of the syntax to express ideas of variance comparing to C# and Kotlin at the moment.

Please also note, due to the length of the article, we have not covered all the generic ideas in Swift. Some important ones that have been left over are Associated Types , Opaque types, Generic Subscripts etc. Similarly, Type projections and Type erasure are also very useful features in Kotlin. If you like to learn more about them, please check out the them via the links provided.

Comments

Sharing is caring!