useClosure() {work, backwards in returnValuesAsInput (backwards, work)}

montyharper

Monty Harper

Posted on October 1, 2023

useClosure() {work, backwards in returnValuesAsInput (backwards, work)}

In my previous post, I wrote about callbacks in Swift, or more specifically using a completion handler to handle the results of a network request. I talked about how closures can be used to compose functions, similar to function composition in algebra. I mentioned the moment of insight that led to my understanding: “a completion handler is a way of returning values.”

Over the past three weeks I’ve been re-learning and solidifying and internalizing that bit of wisdom, so today I want to expand on it a bit.

This is a Weird Idea!

I typically think of a function as a block of code that processes input to return an output. But a closure is a function that can be passed as input into another function. The idea of returning values via the inputs of a closure seems counter-intuitive.

For Example

Here’s how I would normally use a function:

print(“It will take you about \(calculateTimeRemaining(tasksCompleted: tasksArray)) days to complete this course.”)

func calculateTimeRemaining (tasksCompleted: [Task]) -> Int {
     // use the array of completed tasks to calculate n
     return n
}
Enter fullscreen mode Exit fullscreen mode

In this code, tasksArray is passed as input to the function calculateTimeRemaining, which returns a number of days in response. The returned value replaces the function call, so the final output is a sentence: “It will take you about 12 days to complete this course.” (Supposing n comes out to 12 for no particular reason.)

Now let’s look at how the same thing could be accomplished using a closure, providing the output of calculateTimeRemaining not as output but as input to the closure.

calculateTimeRemaining (tasksCompleted: tasksArray, closure: printResult(number:))

func printResult(_ number: Int) {
print(“It will take you about \(number) days to complete this course.”)
}

func calculateTimeRemaining (tasksCompleted:[Task], closure: (Int) -> Void) {
     // use the array of completed tasks to calculate n
     closure(n)  // Instead of a return call, we call the closure, with n for its input!
}
Enter fullscreen mode Exit fullscreen mode

In this case calculateTimeRemaining, rather than returning a value, provides input to its closure function and calls that function. Since printResult was the function we passed in, that’s what gets called, and printResult uses the input provided by calculateTimeRemaining (rather than the output as in the previous example) to insert a number into the printed sentence. Whew.

Why Make It Complicated?

As we talked about last time, if we need to await results, as in results fetched from a network request, then a closure is a good way to return the outcome. The network request function can tie up its own thread making the request, then pass its output as input into a completion handler, which will handle the results back on the main thread.

Also, in Swift a function can take as many inputs as needed, but can only return one output value. If you want it to return more than one output, using a closure is a good option because it allows you to use one function’s inputs as another function’s output. (You could also return a tuple, but that's another story for another day.)

Working Backwards

Setting up functions to work together this way can get confusing. I’ve discovered it’s much easier if you work backwards. Here’s an example…

In my current assignment project I wanted to give a title to a map pin dropped by the user onto a map. This turns out to be a lot more involved than it sounds.

I was mucking about in the tasks involved, getting lost and confused. I knew I needed more than one function because the required reverse-geocoding involves a network request. But I was struggling with how to orchestrate the whole mess into a reasonable title and subtitle for my map pin.

Drawing with arrows connecting boxes representing the different functions involved

Finally I made the above drawing (is this what you call white boarding??) and based on that, I pieced together the following three lines of code:

// Create the new pin
findPlacemark(at: location) {name, placemark in
     self.createPinTitles(poiName: name, place: placemark) {title, subtitle in
          self.createPin(at: placemark, title: title, subtitle: subtitle)
     }
}
Enter fullscreen mode Exit fullscreen mode

These lines call three functions that didn’t actually exist yet at the time. But writing the function calls helped me figure out how one function's output becomes the next function's input.

Note that I used what Swift calls "trailing closure notation," which means the closure I’m passing into a function follows the function call {in a lovely set of curly braces like this.}

I’ve composed two closures here, one inside the other.
So, it’s complicated.

findPlacemark reverse-geocodes the location and also determines whether it’s near a particular point of interest.
Then createPinTitles determines the most specific possible title and subtitle for the pin given the geographic information available. Finally, createPin actually makes the map pin.

When I wrote these three lines, I did it backwards, thinking about what information I needed to create the outcome I wanted. To create a pin I needed a placemark, title, and subtitle. To determine the title and subtitle I needed a placemark and a potential point of interest name. To get the placemark and/or point of interest, I needed to reverse-geocode the location.

Look at the start of each closure:

    {name, placemark
    {title, subtitle
Enter fullscreen mode Exit fullscreen mode

In each case the parameters listed are outputs of the previous function, serving as inputs to the function that follows. Pretty cool, eh?

Once I wrote down the function calls for my non-existent functions, I could then see how the inputs and outputs should flow from function to function. That made it much easier to write the functions themselves.

In Conclusion

I’m still a rank beginner, and I do not claim to have found the best way to accomplish naming a map pin. I’m sure one day I’ll look back on this and laugh at my wackadoo approach. But I’m telling you two things for sure:

  1. A closure is an alternative way to return multiple values from a function.
  2. When planning how to combine functions and closures to accomplish some multi-part task, start with your function calls. Get them working together in a way that makes sense, then write the actual functions.
💖 💪 🙅 🚩
montyharper
Monty Harper

Posted on October 1, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related