Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Type Driven Development in Swift

Type Driven Development in Swift

Type Driven Development in Swift. Ideas for designing programs type-first and leveraging the power of the type system in the Swift programming language.

Johannes Weiss

February 26, 2015
Tweet

More Decks by Johannes Weiss

Other Decks in Technology

Transcript

  1. TYPE DRIVEN DEVELOPMENT
    IN SWIFT
    @JOHANNESWEISS || [email protected]

    View Slide

  2. TDD?

    View Slide

  3. TYDD!

    View Slide

  4. KEY STRATEGIES & BENEFITS
    ▸ laying out the types first ! rapid prototyping & easy refactoring
    ▸ preferring compiler proof to unit test ! correctness & quicker
    ▸ reducing side-effects ! enable (unit) testing & composability

    View Slide

  5. MAKING THE ILLEGAL
    IMPOSSIBLE !
    ALLOWING THE COMPILER TO HELP AS MUCH AS POSSIBLE

    View Slide

  6. BAD EXAMPLE
    struct BookStatusBad {
    let available : Bool
    let lost : Bool
    let lentTo : Person?
    }
    illegal values:
    let ill1 = BookStatusBad(available: true, lost: true, lentTo: TimCook)
    let ill2 = BookStatusBad(available: true, lost: false, lentTo: TimCook)
    let ill3 = BookStatusBad(available: false, lost: false, lentTo: nil)

    View Slide

  7. BETTER
    enum BookStatus {
    case Available
    case Lost
    case LentTo(Person)
    }
    ▸ Goal: Only meaningful values can be created.

    View Slide

  8. BAD EXAMPLE
    func contentsOfDirectoryAtPath(path : String) -> ([String]?, NSError?) {
    [...]
    }
    WHY BAD?
    ▸ illegal values (nil, nil) & ([...], NSError(...))

    View Slide

  9. BETTER!
    enum Result {
    case Success(T)
    case Failure(NSError)
    }
    func contentsOfDirectoryAtPath(path : String) -> Result<[String]>
    func createDirectoryAtPath(path : String) -> Result<()>
    func dataWithJSONObject(obj : AnyObject) -> Result
    * COMPILER BUGS CURRENTLY PREVENT THIS

    View Slide

  10. TEMPORARY WORKAROUND
    class Box {
    let unbox : T
    init(_ value: T) { self.unbox = value }
    }
    enum Result {
    case Failure(NSError)
    case Success(Box)
    init(Success value:T) { self = .Success(Box(value)) }
    init(Failure error:NSError) { self = .Failure(error) }
    }

    View Slide

  11. RESULT COALESCING OPERATOR
    func ??(result: Result, @autoclosure defaultValue: () -> T) -> T {
    switch result {
    case .Success(let boxedValue):
    return boxedValue.unbox
    case .Failure(let _):
    return defaultValue()
    }
    }
    print(Result(Success:"Hello ") ?? "foo")
    print(Result(Failure: someError) ?? "World!\n")

    View Slide

  12. MAPPING
    extension Result {
    func map(f: T -> A) -> Result {
    switch self {
    case .Success(let boxedValue):
    return Result(Success: f(boxedValue.unbox))
    case .Failure(let problem):
    return Result(Failure: problem)
    }
    }
    }
    print(Result(Success:966).map { $0 / 42 }) /* Result(Success: 23) */

    View Slide

  13. WHEN DESIGNING TYPE FIRST,
    YOU OFTEN HAVE TO
    (TEMPORARILY) FILL HOLES
    ⛳️

    View Slide

  14. UNDEFINED CAN FILL HOLES
    func undefined(_ message:String="") -> T {
    fatalError("undefined \(message)")
    }
    Can be used for simple things like
    let a : String = undefined("special string")
    let b : Int = undefined("some int")
    let t = NSTimer(timeInterval: undefined(), target: undefined(),
    selector: undefined(), userInfo: undefined(),
    repeats: undefined())

    View Slide

  15. FUNCTION HOLES
    func parseIntFromString(input : String) -> Int? {
    let parsedInt = (undefined() as String -> Int?)(input)
    return parsedInt
    }
    func mayFail(x : Int) -> Result
    func addUpSuccesses(xs : [Int]) -> Int {
    let combine : (Int, Result) -> Int = undefined("fancy +")
    return map(xs, mayFail).reduce(0, combine:combine)
    }
    -> fatal error: undefined fancy +: file main.swift, line 214

    View Slide

  16. CONCENTRATING ON SUCCESS FIRST
    func rm(path:String) -> Result<()> {
    if NSFileManager.defaultManager().removeItemAtPath(path,
    error:undefined("NSError pointer")) {
    return Result(Success:())
    } else {
    return Result(Failure:undefined("the NSError returned above"))
    }
    }

    View Slide

  17. HANDLING THE IMPOSSIBLE
    func lexicographicallyLowestString(x:String, y:String, z:String) -> String {
    let xyz = [x, y, z]
    return xyz.sorted({ $0 < $1 }).first ??
    undefined("the impossible happened!")
    }

    View Slide

  18. !
    PHANTOMS

    View Slide

  19. PHANTOM TYPES
    protocol Currency {}
    enum GBP : Currency {}
    enum EUR : Currency {}
    enum USD : Currency {}
    struct Money {
    let amount : NSDecimalNumber
    }

    View Slide

  20. PHANTOM TYPE EXAMPLE
    func convertGBPtoEUR(gbp:Money) -> Money {
    let forex : NSDecimalNumber = NSDecimalNumber(mantissa: 133263,
    exponent: -5,
    isNegative: false)
    return Money(amount:gbp.amount.decimalNumberByMultiplyingBy(forex))
    }

    View Slide

  21. THANK YOU!
    @JOHANNESWEISS || [email protected] || GITHUB.COM/WEISSI

    View Slide