Skip to content

Instantly share code, notes, and snippets.

@rbobbins
Last active May 15, 2022 21:08
Show Gist options
  • Save rbobbins/de5c75cf709f0109ee95 to your computer and use it in GitHub Desktop.
Save rbobbins/de5c75cf709f0109ee95 to your computer and use it in GitHub Desktop.
Notes from "Protocol-Oriented Programming in Swift"

PS: If you liked this talk or like this concept, let's chat about iOS development at Stitch Fix! #shamelessplug

Protocol-Oriented Programming in Swift

Speaker: David Abrahams. (Tech lead for Swift standard library)

  • "Crusty" is an old-school programmer who doesn't trust IDE's, debuggers, programming fads. He's cynical, grumpy.

  • OOP has been around since the 1970's. It's not actually new.

  • Classes are Awesome

    • Encapsulation
    • Access control
    • Abstraction
    • Namespace
    • Expressive syntax
    • Extensibility
  • Actually, types are awesome. Three features (access control, abstraction, namespaces) allow us to manage complexity. You can do all that with structs and enums.

  • Crusty's 3 complaints about classes:

  1. Automatic sharing. Two classes can have a reference to the same data, which causes bugs. You try to copy things to stop these bugs, which slows the app down, which causes race conditions, so you add locks, which slows things down more, and then you have deadlock and more complexity. BUGS! This is all due to implicit sharing of mutable state. Swift collections are all value types, so these issues don't happen
  2. Inheritance is too intrusive. You can only have 1 super class. You end up bloating your super class. Super classes might have stored properties, which bloat it. Initialization is a pain. You don't want to break super class invariants. You have to know what to over ride, and how. This is why we use delegation in Cocoa
  3. Lost type relationships. You can't count on subclasses to implement some method, e.g:
class Ordered {
	func precedes(other: Ordered) -> Bool { fatalError("implement me") }
}

class Number: Ordered{
	var value: Int
	override func precedes(other: Ordered) -> Bool {
		return value < other.value //THIS IS THE PROBLEM! WHAT TYPE IS ORDRED?! Does it have a value? Let's force type cast it
	}
}

PROTOCOLS SOLVE CRUSY'S PROBLEMS!

  • Swift is a Protocol-Oriented language

V1:

protocol Ordered {
	func precedes(other: Ordered) -> Bool // No implementation, no runtime error!
}

struct Number: Ordered {
	var value: Double = 0
	func precedes(other: Ordered) -> Bool {
		return self.value < (other as! Number).value
	}
}

V2:

protocol Ordered {
	//Self is a placeholder for the type that will conform to that protocol. So cool!
	func precedes(other: Self) -> Bool
}

struct Number: Ordered {
	var value: Double = 0
	func precedes(other: Ordered) -> Bool {
		return self.value < (other as! Number).value
	}
}

func binarySearch<T: Ordered>(sortedKeys:[T], forKey k: T) -> Int {...}
  • Basically, use Self in protocols. Guarentees more build-time safety.

Another example:

protocol Drawable {
	func draw(renderer: Renderer)
}

protocol Renderer {
	func moveTo(p: CGPoint)
	func lineTo(p: CGPoint)
	func arcAt(center....)
}

struct Circle: Drawable { ... }
struct Polygon: Drawable { ... }
struct Diagram: Drawable {
	var elements: [Drawable]
	func draw(render: Renderer) {
		for e in self.elements {
			e.draw(renderer)
		}
	}
}

//For test purposes
struct TestRenderer: Renderer {
	//Implementations all print the points
}

//For drawing purposes. You can extend a class to conform to a protocol
extension CGContext: Renderer {
	// Implementations actually perform drawing
}

Protocols and geneircs for testability

  • Way better than mocks, which don't play well with Swift's type system

Swift 2:

  • Lets you extend a protocol with a default implementation of a method. Why would you do this?
    • Requirements (ie required method in a protocol) creeate customization points
    • Sometimes you want custom behavior, sometimes you don't.
  • You can constrain an extension (eg: extension Ordred where Self: Comparable { ... })
  • Generic beautification:
extension CollectionType where Index == RandomAccessIndexType, Generator.Element: Ordered {
	//Implementation of method doesn't need angled brackets
}
  • Always make value types Equatable. Why? Go to value types talk. (Probably because it increases testability).
    • Can't implement == for Drawable type. (I missed why?)
    • Require Drawable structs to implement func isEqualTo(other: Drawable) -> Bool
    • Then, `extension Drawable where Self: Equtable { // check whether (missed this code) }

When to use classes

  • Copying or comparing instances doesn't make sense, i.e Window
  • Lifetime of the instance is tied to an external side effect, i.e files appearing on disk. Why? Values are created liberally by compiler.
  • Instances are just sinks w/ write-only access to external state -- i.e our Renderer implementations could have been a class.
  • Framework expects you to subclass or pass an object.
  • Consider making classes final, so they can't be subclasses. If you want to subclass it - consider abstracting behavior into a protocol
@wilsolutions
Copy link

tkx for sharing

@karagraysen
Copy link

Thanks for sharing! 😄

@thinkscientist
Copy link

Thanks!

@RuiAAPeres
Copy link

Actually, that gist is wrong. It should be:

protocol Ordered {
    //Self is a placeholder for the type that will conform to that protocol. So cool!
    func precedes(other: Self) -> Bool
}

struct Number: Ordered {
    var value: Double = 0
    func precedes(other: Number) -> Bool {
        return self.value < other.value
    }
}

Otherwise, the Self constrain is pointless.

@eMdOS
Copy link

eMdOS commented Jun 29, 2015

@acicartagena
Copy link

thanks for the notes

@hsavit1
Copy link

hsavit1 commented Oct 1, 2015

thanks for that fantastic comment @eMdOS

@abbottmg
Copy link

abbottmg commented Oct 1, 2015

Regarding your line "Can't implement == for Drawable type. (I missed why?)":

They're actually saying you can't implement == for [Drawable], which is an array of heterogenous value types that each conform to Drawable. As mentioned earlier, Equatable's == has a Self requirement on the two arguments, which disqualifies heterogenous arrays from automatically getting the implementation of Equatable for arrays. In order to have equality on such arrays, you basically have to fall back to the way ObjC classes test for equality. I'm working from memory (and sans compiler), but I believe the default implementation is basically:

extension Drawable where Self: Equatable {
  func isEqualTo(other: Drawable) -> Bool {
    guard let otherWithMatchingType = other as? Self else { return false }
    return self == otherWithMatchingType
  }
}

@KimBin
Copy link

KimBin commented May 16, 2016

protocol someProtocol {
var p1:string
}

extension someProtocol {
func configP1(value:string){
self.p1 = value //error
}

if the Self can use the protocol's properties ,it will be perfect

@rtoal
Copy link

rtoal commented May 30, 2016

Nice summary!

FYI typo: CRUSY'S :)

@balazsnemeth
Copy link

Here is a trich how to extend all NSManagedObject subclass, maybe it's useful for someone:
https://gist.github.com/balazsnemeth/756fa0b6ca9823bcbff0241a66f0b1ca

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment