What is the distinction between generics being supported at the language level like in Swift, compared to at the framework level?

Isn't using an NSOrderedSet in Objective-C also effectively generic?

In what sense do mean that NSOrderedSet is "effectively generic"? Because it can contain objects of any type?

Yup.

NSOrderedSet can contain objects of any type at the same time. So you cannot be certain that it contains only type T — it can contain U and V as well, all at the same time.

That is called type erasure, and is orthogonal to generics.

5 Likes

That's not what "generics" mean. It's simply untyped, similar to a dynamically typed language like Python.

In a similar vein, C would be mistakenly labelled as having generics merely because you can make a data structure that stores void * (untyped pointers, which can point to any kind of data).

The point of generics is to allow you to express relationships between types. For example, the append function of Array (which is generic over any type Element) has a parameter of type Element, which makes it impossible to say, add a String to an Array<Int>.

It's actually possible to implement this in a library, solely at runtime. Imaging that Swift didn't have generics, you could have implemented something similar to Array like:

class TypedArray {
	private let elementType: Any.Type
	private var buffer = Array<Any>() // Array is generic, but pretend it wasn't.
	
	init(of elementType: Any.Type) {
		self.elementType = elementType
	}
	
	public func append(_ newElement: Any) {
		typeCheck(newElement)
		self.buffer.append(newElement)
	}
	
	public subscript(_ index: Int) -> Any {
		get { return buffer[index] }
		set {
			typeCheck(newValue)
			buffer[index] = newValue
		}
	}
	
	private func typeCheck(_ value: Any) {
		// This only checks for exact matches in types, which means subclasses
		// won't work properly
		precondition(
			type(of: value) == elementType,
			"Type error! You tried to append an \(type(of: value)) to a TypeArray expecting only \(elementType)!"
		)
	}
	
	// Ommitting conformance to Sequence, Collection, RangeReplacableCollection, ExpressibleByArrayLiteral, for brevity
	// ...
}

let myTypedArray = TypedArray(of: Int.self)
myTypedArray.append(123)
myTypedArray.append(456)
myTypedArray.append(789)
print(myTypedArray[2])

myTypedArray.append("This is not an int!") // 💥

But this has a ton of issues:

  1. Correctly typed code requires you to remember to apply runtime type checking everywhere.
  2. Having dynamic type checking everywhere is slow
  3. It "fails late". Rather having the compiler statically analyze your program at compile-time and be able to prove that its types are correct, this can only be checked at runtime when running the program. Worse yet, if there's a code path you don't exercise in your tests, you might only hit it in prod.
4 Likes