Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
183: Recursive Generic Constraints
I was working on a system recently where I needed to combine data from two different sources—think of it like merging two user profiles or two configuration files. My first instinct was to create a simple protocol to handle this. I figured, if something is "mergeable," it should just take another mergeable thing and return the result.
The "Too Generic" Trap
Here is how I started. I defined a protocol called Mergeable and gave it a method that takes another Mergeable object.
protocol Mergeable {
func merge(with other: Mergeable) -> Mergeable
}
struct UserProfile: Mergeable {
var name: String
var points: Int
func merge(with other: Mergeable) -> Mergeable {
// I have a problem here...
return self
}
}
As soon as I started implementing this in UserProfile, I hit a wall. Inside the merge function, the other parameter is typed as Mergeable, not UserProfile. To actually access other.points, I'd have to force-cast it using as? UserProfile. That's clumsy, it's error-prone, and it completely defeats the purpose of using generics.
Even worse, nothing stops me from accidentally trying to merge a UserProfile with a SystemSettings object, as long as both conform to Mergeable. That's a recipe for a runtime crash.
Introducing the Recursive Constraint
I need the compiler to guarantee that other is the exact same type as the instance currently calling the method. In Swift, we do this using the Self keyword within the protocol definition. This is what we call a recursive generic constraint because the protocol refers back to the type that eventually adopts it.
Let's adjust the protocol:
protocol Mergeable {
func merge(with other: Self) -> Self
}
Now, look what happens when I implement it in UserProfile. I don't need to cast anything anymore:
struct UserProfile: Mergeable {
var name: String
var points: Int
func merge(with other: UserProfile) -> UserProfile {
return UserProfile(
name: self.name,
points: self.points + other.points
)
}
}
The compiler now understands that if you are calling merge on a UserProfile, the argument must be another UserProfile. If I try to pass a different Mergeable type in there, the code won't even compile. This is much safer.
The Generic Function Wall
Everything seemed great until I tried to write a helper function to merge a list of these items. I tried this:
func mergeAll<T: Mergeable>(items: [T]) -> T? {
guard var result = items.first else { return nil }
for item in items.dropFirst() {
result = result.merge(with: item)
}
return result
}
Surprisingly, this works perfectly. But here is where you'll likely get tripped up: what happens if you try to use Mergeable as a type itself? If you try to create an array of [Mergeable], Swift will throw a compiler error: "Protocol 'Mergeable' can only be used as a generic constraint because it has 'Self' requirements."
This is the trade-off. By using Self, you've made the protocol incredibly powerful for generic functions (like mergeAll), but you've lost the ability to use the protocol as a standalone type. You can't have a variable of type Mergeable because the compiler can't know which specific version of Self it's dealing with at runtime.
Why this matters for your architecture
I've seen a lot of developers fight this error for hours. The key is to realize that recursive constraints are for behavioral contracts, not for type erasure. Use them when you need to ensure that two objects interacting are of the same type—like in Comparable (where you compare Self to Self) or in our Mergeable example.
If you find yourself needing an array of different mergeable types, you'll need a different pattern (like a wrapper or a type-erased box), but for strict, type-safe operations, the recursive Self constraint is your best tool.
📋 Practical Task
Build a Type-Safe Coordinate Blender
You are building a graphics engine that needs to "blend" two coordinates of the same type (e.g., two 2D points or two 3D points) by averaging their values.
- Create a protocol named
Blendable. It should have a recursive generic constraint requiring a methodblend(with other: Self) -> Self. - Implement this protocol in a struct called
Point2D(containingxandyas Doubles). Theblendmethod should return a newPoint2Dwhere the coordinates are the average of the two points. - Implement this protocol in a struct called
Point3D(containingx,y, andzas Doubles) using the same averaging logic. - Write a generic function called
blendSequence<T: Blendable>(points: [T]) -> T?that takes an array of blendable items and iteratively blends them all into one final result. - Test your code by blending an array of
Point2Dobjects and ensuring the compiler prevents you from blending aPoint2Dwith aPoint3D.
There are no comments for now.