Skip to content

Instantly share code, notes, and snippets.

@rbobbins
Last active June 11, 2024 22:11
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
@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