5

So I love declaring variables to hold return value and then return said variable on next line, thus making it easy to debug my code, I can just set a breakpoint at the return line and see what value it returns. I use this everywhere and it makes all my code so much easier to debug.

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let cellCount = models.count
    return cellCount
}

But then you have the scenario where you have optionals and different conditions that have to be met in order for your method to makes sense. The guard statement is great for making sure some conditions are met, while not introducing pyramids of doom.

But the problem with early returns is that you get at least two exit points (because guard requires a return in this context) from your method, which makes it harder to debug.

// Instantiate using dependency injection
private let reachability: ReachabilityProtocol
private let apiClient: APIClientProtocol

    // Returns true if could start login request, else false 
    func loginUser(username: String, password: String) -> Bool {
        defer {
             // Not possible, would be nice! Return value would be an implicitly declared variable
             // exaclty like the variables 'newValue' and 'oldValue' in property observers!
            print("return value: \(returnValue)")
        }
        guard reachability.isOnline && !username.isEmpty && !password.isEmpty { return false }
        apiClient.loginUser(username, password: password)
        return true
    }

It would be awesome if Swift 3.X would make the defer statement able to catch the return value, wouldn't it?

This would make debugging so much easier, whilst still making use of guard and early returns. I have not insight what so ever in writing compilers etc, but it feels like this would not be so hard to implement in coming versions of Swift?

Can you come up with a different way of achieving a single point to read return value of a method with multiple exit points? (Without having to wait for my suggested improvement to defer?)

Edit:
My example above with the login is not a perfect example, sorry, why would I write code like that? Haha! But there are many other similar scenarios, maybe something like this, using do-try-catch also makes code hard to debug:

// We don't know the return value of this function! Makes it hard to debug!
func fetchUserByFirstName(firstName: String, andLastName lastName: String, fromContext context: NSManagedObjectContext) -> User? {
    defer {
         // Not possible, would be nice! Return value would be an implicitly declared variable
         // exaclty like the variables 'newValue' and 'oldValue' in property observers!
        print("return value: \(returnValue)")
    }

    guard !firstName.isEmpty else { print("firstName can't be empty"); return nil }
    guard !lastName.isEmpty else { print("lastName can't be empty"); return nil }
    // Imagine we have some kind of debug user... Does not really make sense, but good for making a point.
    guard firstName != "DEBUG" else { return User.debugUser }
    let fetchRequest = NSFetchRequest(entityName: Users.entityName)
    let predicate = NSPredicate(format: "firstName == \(firstName) AND lastName == \(lastName)")
    fetchRequest.predicate = predicate
    do {
        let user = try context.executeFetchRequest(fetchRequest)
        return user
    } catch let error as NSError {
        print("Error fetching user: \(error)")
    }
    return nil
}
Sajjon
  • 8,938
  • 5
  • 60
  • 94

1 Answers1

1

I like your suggested improvement to Swift to have defer capture the return value.

Here is something that will work, but it isn't perfect because it requires a little extra work (and code clutter), but you can do it manually by declaring returnValue with let at the top of your function, giving it the same type that the function returns. Then, replace all of your return <something> with returnValue = <something>; return returnValue.

By declaring returnValue with let, Swift will let you know if you forget to assign returnValue before leaving the function. So if you add a new return to your function, your function won't compile until you assign the returnValue. You'll see the error: error: constant 'returnValue' used before being initialized.

func fetchUserByFirstName(firstName: String, andLastName lastName: String, fromContext context: NSManagedObjectContext) -> User? {
    let returnValue: User?
    defer {
        print("return value: \(returnValue)")
    }

    guard !firstName.isEmpty else { print("firstName can't be empty"); returnValue = nil; return returnValue }
    guard !lastName.isEmpty else { print("lastName can't be empty"); returnValue = nil; return returnValue }
    // Imagine we have some kind of debug user... Does not really make sense, but good for making a point.
    guard firstName != "DEBUG" else { returnValue = User.debugUser; return returnValue }
    let fetchRequest = NSFetchRequest(entityName: Users.entityName)
    let predicate = NSPredicate(format: "firstName == \(firstName) AND lastName == \(lastName)")
    fetchRequest.predicate = predicate
    do {
        let user = try context.executeFetchRequest(fetchRequest)
        returnValue = user; return returnValue
    } catch let error as NSError {
        print("Error fetching user: \(error)")
    }
    returnValue = nil; return returnValue
}

Alternatively (just brainstorming here...), put your function that has multiple exit points into an inner function, and then call it:

func fetchUserByFirstName(firstName: String, andLastName lastName: String, fromContext context: NSManagedObjectContext) -> User? {

    func innerFunc(firstName: String, andLastName lastName: String, fromContext context: NSManagedObjectContext) -> User? {

        guard !firstName.isEmpty else { print("firstName can't be empty"); return nil }
        guard !lastName.isEmpty else { print("lastName can't be empty"); return nil }
        // Imagine we have some kind of debug user... Does not really make sense, but good for making a point.
        guard firstName != "DEBUG" else { return User.debugUser }
        let fetchRequest = NSFetchRequest(entityName: Users.entityName)
        let predicate = NSPredicate(format: "firstName == \(firstName) AND lastName == \(lastName)")
        fetchRequest.predicate = predicate
        do {
            let user = try context.executeFetchRequest(fetchRequest)
            return user.first as? User
        } catch let error as NSError {
            print("Error fetching user: \(error)")
        }
        return nil
    }

    let returnValue = innerFunc(firstName, andLastName: lastName, fromContext: context)
    print("return value: \(returnValue)")
    return returnValue
}
vacawama
  • 150,663
  • 30
  • 266
  • 294
  • Yes exactly what I usually do! :) thank you for taking the time and suggesting this though! :) I guess this is the only way at the moment... – Sajjon Jul 15 '16 at 12:11
  • Added second solution that is worse ;-) – vacawama Jul 15 '16 at 12:14
  • Wow! I didn't even know it was possible to declare a function within a function! That is a very creative solutions! – Sajjon Jul 15 '16 at 12:15