CodeGym /Courses /Swift SELF /Generic types — struct Stack<T> and the basic API

Generic types — struct Stack<T> and the basic API

Swift SELF
Level 43, Lesson 4
Available

1. The structure of Stack<T>: type parameter and storage

If you are just starting to program, it is very easy to fall into a trap: “Oh, I need a container for Int. I will write IntStack now. Then I will need one for String — I will write StringStack. Then for Book — you get the idea…”. The problem is that the code multiplies like rabbits that were forgotten in the cage. We want to write the container logic once — and use it with any type.

A stack (Stack) is good because it has a simple and clear contract: last in, first out. This rule is called LIFO (Last In — First Out), but today we are not going to get lost in abbreviations: imagine a stack of plates. You add from the top — you remove from the top as well. No magic, no philosophy.

Important: in Swift’s standard library, a stack is not exposed as a separate type, but it is easy to build on top of Array. That is how real projects do it: either they use an array as a stack, or they write a small wrapper type to make the API more “honest” and protected.

What is a generic type and how to read struct Stack<T>

In previous lectures you already saw generic functions — for example, when a function works with different types, and instead of a concrete type it uses T. In Swift, generics are basically built into the language’s “way of life,” and even many examples in the official materials show generic signatures with a type parameter and where constraints.

Now let us apply the same idea at the type level.

When you see:


struct Stack<T> {
    // ...
}

this is read roughly like this: “Stack is a structure parameterized by type T.” In other words, T is not a concrete type, but rather a “slot for a type.” Until we substitute something like Int or String, Stack<T> is not quite a finished thing; it is more of a template.

As soon as we write Stack<Int>, we “concretize” the generic type. We get a normal concrete type: a stack of integers. Likewise, Stack<String> is a stack of strings. And so on.

To avoid confusion, keep this simple diagram in mind:

flowchart LR
    A["Stack<T> (template)"] --> B["Stack<Int> (concrete type)"]
    A --> C["Stack<String> (concrete type)"]
    A --> D["Stack<Book> (concrete type)"]

And yes: T is just a name. You can call it Element, you can call it Item, you can call it Potato. The compiler does not care, but people do, so in educational examples T is usually used as the classic “Type.”

Internal storage: [T] and why it looks so simple

Now comes the moment when you may think: “Wait, that is it?” Yes, programming sometimes feels like magic, but this is more like the kind of magic where “I put the keys in my pocket, not in the freezer.”

We want to store stack elements inside an array. Since the stack is universal, the array should also be universal: [T].

struct Stack<T> {
    private var items: [T] = []
}

Two important things happen here.

First, items is an array of elements of type T, that is, “whatever it happens to be.” If T = Int, then [T] becomes [Int]. If T = String, then [T] becomes [String].

Second, private is our protection. We tell external code: “Do not poke around in my array with your hands. You can only use the API I give you.” This is an important habit: if you make items public, any code can do whatever it wants (for example, remove an element “from the middle”), and the stack will stop being a stack.

Where does T live in Stack<T>: properties and methods

At this stage it is useful to stop and literally trace with your eyes where exactly T is used and how it “materializes.”

Inside Stack<T> we used T in three places: in the array [T], in the method parameter push(_ value: T), and in the result of pop() -> T?.

Here is the same code, but with small comments about “what matches what”:

struct Stack<T> {
    private var items: [T] = []      // store elements of type T

    mutating func push(_ value: T) { // accept T
        items.append(value)
    }

    mutating func pop() -> T? {      // return T? (or nil)
        items.popLast()
    }
}

Now imagine that we wrote Stack<String>. Then the compiler mentally substitutes T = String, and we get something like this “equivalent” version, although in reality Swift does not create a new type manually; this is just a model for understanding:

// imaginary result of "substitution"
struct StringStack {
    private var items: [String] = []

    mutating func push(_ value: String) {
        items.append(value)
    }

    mutating func pop() -> String? {
        items.popLast()
    }
}

This is a very important intuition: a generic type is not “a type without a type.” It is a type that becomes ordinary once you substitute concrete data.

3. The stack’s basic API: push and pop

Now we will add two operations to the type.

The push operation puts an element on top of the stack. That means it changes the internal array, so the method must be mutating.

The pop operation removes the top element. The question is: what if the stack is empty? Today we will choose the most beginner-friendly contract: return Optional, that is, T?. Empty means nil. In the later lectures of this day you will compare this with approaches using throws and Result, but today we need to build the foundation.

struct Stack<T> {
    private var items: [T] = []

    mutating func push(_ value: T) {
        items.append(value)        // place "on top"
    }

    mutating func pop() -> T? {
        return items.popLast()     // remove "from the top"
    }
}

Notice a nice detail: Array already gives us popLast(), which returns an Optional. In other words, Swift is almost saying: “Here, I already thought about the empty array for you.” And that is one of the reasons why a stack is so convenient to build on top of an array.

Mini-check for Stack<Int>:

var numbers = Stack<Int>()
numbers.push(10)
numbers.push(20)

let value = numbers.pop()
print(value as Any) // Optional(20)

Why as Any? Because print prints Optional(20) only if we explicitly print the optional as “anything.” Otherwise you will quickly want to write !, and today we agree not to live in “as long as it compiles” mode.

4. Creating a stack: Stack<Int>() vs Stack() and type inference

One of the most common beginner questions sounds like this: “Why can I sometimes write Stack(), and sometimes the compiler complains?”

Answer: because the compiler needs context to understand what T is equal to.

If you write this:

var s = Stack<Int>()   // explicitly said here: T = Int

everything is clear.

If you write this:

var s: Stack<String> = Stack()  // the type on the left hints that T = String

that is also fine: on the right, Stack() gets the context “this must be Stack<String>.”

But this is bad:

// var s = Stack() // the compiler does not know what T is

The compiler is not a telepath. It cannot guess whether you want a stack of Int, String, or Book. It needs at least one clue.

Mini-example with different types:

var words: Stack<String> = Stack()
words.push("Swift")
words.push("6.2")

print(words.pop() as Any) // Optional("6.2")
print(words.pop() as Any) // Optional("Swift")
print(words.pop() as Any) // nil

Notice that Stack<T> does not care at all what is inside. It does not “understand strings.” It only knows how to honestly push and pop elements.

5. Applying Stack<T> in a CLI application: action history

Now let us take a small but realistic step: apply a stack in our “educational universe” with a library. Even if your application is still very simple (say: a list of books, adding/removing), it quickly develops a need to store action history. For example, to implement “undo the last action.”

We are not going to build a full undo engine (that easily becomes a separate weekend project), but we will make a scaffold: we will store “what we just did” in a stack.

To begin with, let us define a very simple book model:

struct Book {
    let id: Int
    let title: String
}

Now let us define an action type. Since you are already familiar with enum and associated values, this will look natural:

enum LibraryAction {
    case added(Book)
    case removed(Book)
}

Now in top-level code (or in main) we can use Stack<LibraryAction>:

var history = Stack<LibraryAction>()

let book = Book(id: 1, title: "Swift for People Who Keep Holding On")
history.push(.added(book))

let last = history.pop()
print(last as Any) // Optional(added(Book(id: 1, title: "Swift for People Who Keep Holding On")))

For now this looks like “so what?” And the important “what” here is this: we got a universal container that can be applied to domain types. We did not write ActionStack, we did not write BookStack, and we did not multiply entities. We wrote one stack — and use it in different places.

And this is where generics stop being “a topic from a textbook” and become ordinary engineering practice.

By the way, similar ideas appear in discussions and in the design of more complex systems, where abstractions make it possible to assemble type-safe containers and avoid “type soup.”

6. What we have already implemented: the minimal Stack<T> contract

Before moving on, let us record the minimum that makes our Stack<T> useful. The table here is not bureaucracy; it is here so that the idea sticks in your head: an API is a contract.

Element What it looks like What it means in plain English
Generic parameter
Stack<T>
“A stack for elements of type T”
Storage
private var items: [T]
“Inside, there is an array, but we do not expose it”
Adding
push(_ value: T)
“Put on top”
Extraction
pop() -> T?
“Remove from the top, or nil if empty”

Notice that we have not added peek, count, or isEmpty yet. That is not because they are “hard,” but because today we are staying focused: understand how a generic type is declared, how T lives inside it, and how to make a minimal working API.

7. Typical mistakes

Mistake #1: writing var s = Stack() without type context.
This is one of the most annoying mistakes, because it feels like the compiler is “being picky.” In reality it is honestly saying: “I do not know what T is.” The fix is simple: either write Stack<Int>(), or specify the variable type on the left, for example var s: Stack<String> = Stack().

Mistake #2: forgetting mutating on push and pop.
If a method changes items, it changes the state of the structure, which means it must be mutating. Beginners often see this as an “extra word,” but it is an important part of Swift’s value-type model: a structure cannot quietly mutate itself “out of thin air.”

Mistake #3: making items public “for convenience.”
When you leave items accessible from the outside, you lose the whole point of the Stack type: external code can remove an element from the middle, insert at the beginning, sort the array, and give your stack an identity crisis. The right habit is to hide implementation details and expose only the operations that support the container’s contract.

Mistake #4: returning from pop() not T?, but T, and hiding emptiness with ! or “later somehow.”
A stack can be empty — that is a normal state, not an “exception to the rule.” If you write pop() -> T, you will either have to crash at runtime or invent a “dummy value,” which is even worse. At the basic API level it is much more honest to return Optional and clearly show the caller: “An element may be missing.”

Mistake #5: trying to “explain generics” with complicated words instead of the simple substitution rule.
If you catch yourself saying “parameterization of polymorphism at the type level with constraint inference…” — stop. For a start, simple intuition is enough: T is a slot for a type, and Stack<Int> is the result of substitution. If that picture exists in your head, the rest will build itself.

1
Task
Swift SELF, level 43, lesson 4
Locked
Safe Stack
Safe Stack
1
Task
Swift SELF, level 43, lesson 4
Locked
Words in Reverse Order
Words in Reverse Order
1
Task
Swift SELF, level 43, lesson 4
Locked
Array Reversal
Array Reversal
1
Task
Swift SELF, level 43, lesson 4
Locked
Undo actions
Undo actions
1
Survey/quiz
Generics Constraints, level 43, lesson 4
Unavailable
Generics Constraints
Constraints, where, rethrows, Stack
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION