Skip to main content

Protocols, Opaque return types & Extensions (Day 13)

·1160 words·6 mins

SideQuest - Learn a new language #

I have decided to embark on learning Turbo Native, my first foray into this wasn’t too bad, however I feel like I’m not understanding key concepts so it’s time to go back to the drawing board.

For my next trick, I’m going to learn some swift so that I can learn Turbo Native 🎉

I’ve found a great online resource called 100 Days of Swift - Hacking with Swift and I’m going to try and stick with it. I’m planning on using this site to document little nuances that I need to remember as a set of notes for me to refer back to.

For anyone else reading this, these notes are probably not very useful and I would suggest going to this great resource.

Protocols #

I’m up to this section in the resource How to create and use protocols and for my memory, this is how protocols interact with classes.

Protocols are like interfaces in java. They are used to define a contract that an implementer must adhere to, however they do not implement their methods.

Rules:

  1. Protocols are types, use a Capital letter to name them.
  2. List the methods that are needed in the protocol for an implmenter to comply.
  3. There is no code in the protocol. It is merely a placeholder.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols

protocol SomeProtocol {
  // protocol definition goes here
}
// Use in a struct
struct myStructure: SomeProtocol, AnotherProtocol{
  // structure definition goes here
}
// Use in a class
struct myClass: SomeSuperClass, SomeProtocol, AnotherProtocol{
  // class definition goes here
}

A more concrete example is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
protocol Vehicle {
  var name: String { get }
  var currentPassengers: Int { get set }
  func estimateTime(for distance: Int) -> Int
  func travel(distance: Int)
}
struct Car: Vehicle{
    let name="Car"
    var currentPassengers = 1

    func estimateTime(for distance: Int) -> Int {
        distance / 50
    }
    func travel(distance: Int){
        print("I'm traveling \(distance) km")
    }
    func openSunroof(){
        print("it's sunny")
    }
}
let myCar = Car()
myCar.travel(distance: 100)
myCar.openSunroof()
// I'm traveling 100 km
// it's sunny
class Bicycle: Vehicle {
    let name: String
    var currentPassengers: Int
    init(){
        name = "Bicycle"
        currentPassengers = 1
    }
    func estimateTime(for distance: Int) -> Int {
            distance / 10
    }
    func travel(distance: Int) {
        print("I'm cycling \(distance)km.")
    }
}
var bike = Bicycle()
bike.travel(distance: 100)
// I'm cycling 100km.

The benefit is now that both a car and a bike conform to type Vehicle, we can ignore implementation details and treat them both as similar objects that respond to certain messages.

1
2
3
4
5
6
7
8
9
func getTravelEstimates(using vehicles: [Vehicle], distance: Int) {
    for vehicle in vehicles {
        let estimate = vehicle.estimateTime(for: distance)
        print("\(vehicle.name): \(estimate) hours to travel \(distance)km")
    }
}
getTravelEstimates(using: [myCar, bike], distance: 150)
// Car: 3 hours to travel 150km
// Bicycle: 15 hours to travel 150km

Further good information can be found here: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols

Opaque return types #

I’m up to How to use opaque return types.

Opaque return types are an abstraction layer provided by Swift, instead of return the exact type of a method, we can specify the kind of return type it is. This only works because Swift actually knows what is returned by allows us to abstract up a layer.

1
2
3
4
5
6
7
func getRandomNumber() -> Int {
    Int.random(in: 1...6)
}
func getRandomBool() -> Bool {
    Bool.random()
}
print(getRandomNumber() == getRandomNumber())

As both Booleans and Ints conform to the protocol of Equatable, we can actually write

1
2
3
4
5
6
func getRandomNumber() -> Equatable {
    Int.random(in: 1...6)
}
func getRandomBool() -> Equatable {
    Bool.random()
}

This doesn’t seem that useful, however think about returning a Vehicle protocol rather than a simple class. This suddenly allows you to return from a single method multiple types. This allows you to be flexible in the internals of the method and allow you to keep the external API consistent.

Take Note This doesn’t allow you to compare a boolean to an integer, Swift still knows the underlying type that you’re returning.

Extensions #

I’m up to How to create and use extensions

Extensions allow us to extend any type. Kind of like a concern or mixin in Ruby. These are neat!

Here is the ’normal’ way of trimming whitespace from a text string:

1
2
3
import Cocoa
var quote = "   The truth is rarely pure and never simple   "
let trimmed = quote.trimmingCharacters(in: .whitespacesAndNewlines)

instead, our preferred api would be quote.trimmed(). We can achieve this with extensions.

1
2
3
4
5
6
extension String {
    func trimmed() -> String {
        self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}
print("\"\(quote.trimmed())\"")

Further, you can mutate and update the original variable by declaring with mutating

1
2
3
4
5
6
7
8
9
extension String {
    func trimmed() -> String {
        self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
    mutating func trim(){
      self = self.trimmed()
    }
}
print("\"\(quote.trim())\"")
Take Note when we return a new value, you should follow the idiom of returning ed or ing like reversed(). Use the verb if you are modifying in place.

Protocol Extensions #

I’m up to How to create and use protocol extensions.

Protocols let us define contracts that conforming types must adhere to, and extensions let us add functionality to existing types.

As a trivial example, you can check whether an array is populated with:

1
2
3
4
let guests = ["Mario", "Luigi", "Peach"]
if guests.isEmpty == false {
    print("Guest count: \(guests.count)")
}

It would be nice to be able to have have an API that says Array.isNotEmpty. Protocol Extensions to the rescue

1
2
3
4
5
6
7
let guests = ["Mario", "Luigi", "Peach"]
extension Array {
    var isNotEmpty: Bool {
        isEmpty == false
    }
}
guests.isNotEmpty

Further, this could be used for more types, such as Set, Dictionary. These all conform to a protocol called Collection.

1
2
3
4
5
extension Collection {
    var isNotEmpty: Bool {
        isEmpty == false
    }
}

This technique leads to something that apple calls protocol-oriented programming, we can list some required methods in a protocol and then define a default implementation.

For example, you can:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// define a protocol
protocol Person {
    var name: String { get }
    func sayHello()
}
// define an extension, with a default method
extension Person {
    func sayHello() {
        print("Hi, I'm \(name)")
    }
}
// implement a struct (or class)
struct Employee: Person {
    let name: String
}
// instantiate an instance of the class.
let taylor = Employee(name: "Taylor Swift")
taylor.sayHello()