Learn Generics in Swift. Anwers to what generics are, how to use… | by Taha Bebek | Nov, 2022

Answers to What Are Generics, How and When to Use Them

image by author

Normal programming is a technique for writing reusable code while maintaining type safety. This is a type of polymorphism known as “parametric polymorphism”. Polymorphism means using a single interface or name that works with multiple types.

There are mainly three types of polymorphism:

  • Ad hoc polymorphism. We can define multiple functions with the same name, but different types of functions is called ad hoc polymorphism.
  • Subtype polymorphism. If a function expects a class C, we can also pass in a subclass of C. This is called subtype polymorphism.
  • Parametric polymorphism. If a function has a generic parameter, we can pass it to different types. This is called parametric polymorphism.

One reason for this is less code. instead of writing this:

func square(value: Int) -> Int…
func square(value: Double) -> Double …
func square(value: Float) -> Float …

We can write this:

func square(value: T) -> T …

This way, we can write a single function instead of writing multiple functions to handle different parameter types (function overloading). Here T stands for type. You can put anything in there; it doesn’t have to be T, and it doesn’t matter what we put in. This function can also be defined as:

func square(value: PutAnythingHere) -> PutAnythingHere …

This means that if you pass an object with type x For this function, it will return an object with type x,

func square(value: Any) -> Any

This is not good and won’t work because we can pass any type and return any type, which doesn’t make any sense.

We use generics to express and limit what types a class can work with. For example, when we declare an array, we declare it like this:

var arr: Array
arr = [1,2,3,4,5]

it means that arr is an array of integers. This way, we can’t insert anything but integers into this array. We can define an array of integers because Array has a generic type defined:

public struct Array

If you really want a heterogeneous array that can hold any number of objects, you can define it as:

var arr: Array
arr = [1, “Two”, 5.8, Car(), Calendar(), OperationQueue()]

This is something you should avoid unless you really know what you are doing as it can open a can of worms. The reason for this is that if you want to access the elements in the array, you have to open them like this:

var obj0 = arr[0] as! Int
var obj1 = arr[1] as! String
var obj2 = arr[2] as! Double
var obj3 = arr[3] as! Car
var obj4 = arr[4] as! Calendar
var obj5 = arr[5] as! OperationQueue

You see how problematic and dangerous this is. It looks like your app may crash at any time. Can crash with simple code like this:

var obj0 = arr[0] as! String
error: Execution was interrupted, reason: signal SIGABRT.
Could not cast value of type ‘Swift.Int’ (0x1b9ac80b8)
to ‘Swift.String’ (0x1b9ac6258).

So, stick with a generic type when you create an array.

Our SQUARE function above is problematic because we can pass any number of types to it. what if we pass a string?

func square(value: T) -> T …
square(“Some string here”)

Strings don’t have classes, so it doesn’t make sense. We need to limit our generic to a numeric type like this:

func square(value: T) -> T …

This way, we can only pass values ​​that conform to the numeric protocol. We know that all numeric values ​​are squares, so our function has correct bounds for now, and one cannot pass something that doesn’t make sense. If we try to mess with this by calling:

square(“Some string here”)

The compiler gives this error below and does not allow this to happen:

function ‘square(value:)’ requires that ‘String’ conform to ‘Numeric’

We restricted generic types by restricting them to conforming to a single protocol. We can also use a class that will limit the generic type to a subclass from a specific class.


class SomeClass
var name: String = “class name”

func printClassName(value: T)
print(value.name)

class SomeSubClass: SomeClass

printClassName(value: SomeSubClass())
//prints:
//class name

Functions and methods are not just generic types. We can also have generic structures, classes and enums.

Example 1

Let’s create a stack structure that works with a generic type. we need to define this stack with the type of our name Objectso only we can push, popAnd peek same type.


struct Stack
private var objects: [Object] = []

mutating func push(object: Object)
objects.append(object)

mutating func pop() -> Object?
return objects.popLast()

func peek() -> Object?
return objects.last

Here we have defined a structure, which must have a predefined object type. For example, the stack of functions would be like this:

typealias Work = () -> Void
var stack: Stack = Stack()
stack.push …

Here, we limit our stack to the stack of Work works. We can only push functions onto this stack, specially typed functions Work which doesn’t receive any arguments and doesn’t return anything. We can call functions in this stack like this:

while let work = stack.pop() 
work()

Example 2

Let’s make a SwiftUI View who receives the second View as a subview. View SwiftUI has a protocol, so we need to define a structure that conforms to it View protocol and passes a subview as a generic type that also conforms to View Drafting

struct SomeView: View 
let subview: Content
var body: some View
subview

we can make an example of SomeView passing a Text or one Image as a subview, because both Text And Image according to View protocol, like this:

let someView = SomeView(subview: Text("some text…"))
let anotherView = SomeView(subview: Image("some_image"))

Example 3

here is the definition Optional In Swift:

enum Optional  
case none
case some(Wrapped)

Optional is a common type. When we choose a value for Wrapped, we get a concrete type. For example, optional or optional Both are concrete types.

When we want to convert a generic type to a concrete type, we must choose exactly one concrete type for each generic parameter.

Generics are used in the Swift standard library. For example:

  • Collections like Array, Set and Dictionary.
  • Optional Uses a generic parameter to abstract over its wrapped type.
  • Result There are two generic parameters – one representing a successful value and the other representing an error.
  • Unsafe[Mutable]Pointer It is generic on the type of memory it points to.
  • Key Paths are generic on both their root type and the resulting value type.

Associated type is a placeholder name for the type used as part of the protocol. The actual type for the type concerned is not specified until the protocol is adopted. It is a generic type that is declared in a protocol. This way we can use generics in protocols.

what if we want to define our Stack Protocol instead of Structure? let’s try. If we write the line below:

protocol Stack

Compiler gives this error and tells us how to use generics with protocols. Here’s the message:

An associated type named ‘Object’ must be declared in the protocol ‘Stack’ 
or a protocol it inherits

So, how can we define our stack as a protocol? Like this:

protocol Stack 
associatedtype Object

mutating func push(object: Object)
mutating func pop() -> Object?
func peek() -> Object?

The only difference is, instead of defining protocol Stack …we defined it protocol Stack associatedtype Object , Now, let’s make a Stack structure that conforms to Stack Drafting


struct WorkStack: Stack
typealias Object = Work
private var objects: [Work] = []

mutating func push(object: @escaping Work)
objects.append(object)

mutating func pop() -> Work?
return objects.popLast()

func peek() -> Work?
return objects.last

In WorkStackWe can remove the typealias line because the compiler can infer the associated type from the function declarations and understand that the associated type is Work, So, we can define it like this:

struct WorkStack: Stack 
private var objects: [Work] = []

mutating func push(object: @escaping Work)
objects.append(object)

mutating func pop() -> Work?
return objects.popLast()

func peek() -> Work?
return objects.last

//We can create an instance of WorkStack and use it like this:
var workStack = WorkStack()
workStack.push …
while let work = workStack.pop()
work()

We can make other stacks corresponding to Stack protocols like IntStack, StringStack, CarStack, etc., like how we did above. But what if we want to create a generic stack that conforms to Stack Protocols that can be any specific type of stack? We can get it like this:

struct MyStack: Stack 
private var objects: [Item] = []

mutating func push(object: Item)
objects.append(object)

mutating func pop() -> Item?
return objects.popLast()

func peek() -> Item?
return objects.last

//With MyStack, we can create a stack of any type.
var carStack = MyStack()
carStack.push(Car())

This is great Now we can make any type of stack we want. What if we extend an existing class to conform to a related type of protocol? For example, expand the array to conform Stack Drafting Here’s how we can do it:

extension Array: Stack 
mutating func push(object: Element)
self.append(object)

mutating func pop() -> Element?
return self.popLast()

func peek() -> Element?
return self.last

Above, we are using generic type Element of array as related type Object Of Stack Drafting the compiler infers that Array.Element == Stack.Object, remember how Array And Stack It is declared:

public struct Array
protocol Stack
associatedtype Object

what if we pass stack Work Example for a function? What happens if we write the below code? (Keyword inout is to modify the stack that we pass, not related to our topic).

func executeStack(stack: inout Stack) 
...

We get this error:

Use of protocol ‘Stack’ as a type must be written ‘any Stack’

here we can use any either some Keyword. The difference between the two is another matter. if we write this some Stack it means something that conforms to protocol Stack, Let’s try calling the functions like this:

func executeStack(stack: inout some Stack) 
while let work = stack.pop()
work()

For the above code, the compiler gives this error:

Cannot call value of non-function type ‘(some Stack).Object’

This makes a lot of sense because the compiler doesn’t know that the items in this stack are ‘functions’ that it can call. To make it clear to the compiler, we need to declare it like this:

func executeStack(stack: inout T) where T.Object == Work 
while let work = stack.pop()
work()

Above, we are telling the compiler that the parameter stack corresponds to Stack protocol, where its Objectis an example of Work, It works, but it’s a bit ugly. We can also define it as (with a condition):

func executeStack(stack: inout some Stack)
while let work = stack.pop()
work()

condition is that we need to declare our Stack The primary associated type of protocol, such as:

protocol Stack 
associatedtype Object

above, Stack tells the compiler Object is the primary associated type of Stack Drafting

Suppose we want to merge two stacks. Here’s how we can declare a function that does this:

func merge(stack1: some Stack, 
stack2: some Stack) -> some Stack

return mergedStack

We have used a WHERE clause in the example above, which is unnecessary as we have a better option of using a primary associated type, where clause is required for the relevant extension.

Let’s say we want to write an extension for our MyStack To add functionality to encode itself. Maybe we want to save it to disk. To encode, the stack and its items must be consistent Encodable Drafting Here’s how we define it:

extension MyStack: Encodable where Item: Encodable 
func encode() -> Data?
try? JSONEncoder().encode(self)

As you can see, we used where clause to limit the type concerned to being encodable. So, this extension only applies to MyStack Instances in which they and their items are encodable. For all others, this extension does not exist.

For example, because String conforms to Encodable protocol, we can use it like this:

var stringStack = MyStack()
stringStack.push(object: “a string”)
print(stringStack.encode())
prints: Optional(20 bytes)

Sometimes, instead of conforming to a protocol, we may need a specific type of corresponding type. For example, if we want to write an extension only for heaps that have Work As an associated type, we can declare something like this:

extension MyStack where Item == Work 
mutating func execute()
while let work = self.pop()
work()


Starting with Swift 5.7, it is also possible to write this using angle brackets:

extension MyStack 
mutating func execute()
while let work = self.pop()
work()


We can put many restrictions with where clause. Something like this:

extension Foo where T: Sequence, T.Element == Character 
func specialCaseFoo() …

Generic typeleasis allows you to provide names for existing nominal generic types or non-nominal types (for example, tuples, functions, etc.) with generic parameters.

typealias StringDictionary = Dictionary
typealias DictionaryOfStrings = Dictionary
typealias IntFunction = (T) -> Int
typealias Vec3 = (T, T, T)
typealias BackwardTriple = (T3, T2, T1)

you can see This Video for implementation details of generics in Swift.

Leave a Reply

×
×

Cart