Learning Through Abstraction

Monty Harper - Sep 11 '23 - - Dev Community

Or, How College Algebra Helped Me Understand Networking Callbacks

Udacity’s iOS Development course recently plunged me into the world of network requests. We retrieved random dog photos from the dog API and lists of movies from TMDB. Now I’m tasked with pulling photos from flickr.

So today I’m writing about an essential pattern in network request code: the callback. I’m also examining my own learning process, which hopefully will help you with yours.

Confused About My Own Confusion

Swift’s networking code overwhelmed me at first - a million new concepts coming at me all at once. While some of it started to make sense, I kept feeling I was missing something. In an attempt to put a finger on exactly what, I wrote this heading in my notes:

“DispatchQueue, Queues, async, escaping, closures with network requests?”

There I collected bits of understanding until it hit me…

Aha Moment

Here’s what I wrote down that finally untangled the knot:

“Insight: a completion handler is a way of returning values!”

I typed furiously, laying out an explanation to myself. It quickly became clear I was explaining composition of functions, a concept I teach my algebra students every semester! At the same time, I now realize, I was working out the design pattern known as a “callback.”

Learning Through Abstraction

Design patterns like callbacks are not Swift-specific, but show up in every language, and “composition of functions” as a concept is even more universal.

My breakthrough in understanding came from thinking abstractly about the specific operations Udacity wanted me to code into their example apps. When you can see the bigger picture, it helps clarify the purpose and intent behind the details. It gives you a way to mentally organize the concepts you’re learning.

The Problem With Network Requests

When you make a network request, you ask a distant server for a piece of information. If you can’t do the next thing until the data arrives, your code sits there waiting, and your user is likely to notice an annoying lag. Any big calculation or time-intensive process could cause your app to become temporarily unresponsive. That’s not good!

The Solution

Enter asynchronous, or multi-thread programming. You can make your device do two things at once: await network request results on one thread, while keeping your user interface responsive on another.

This brings up a new problem, though - with tasks running on multiple threads, you never know for sure in what order things will happen. That’s where the callback comes in.

When a network request finishes fetching data on the side, you “call back” to the main thread with the results.

And, when you set up a callback, you’re actually composing three functions.

Algebraic Composition of Functions

In algebra, a function typically does some calculation to an input number x. For example if f(x) = 2x, the output of the function is double the input.

If I set up three functions like so:

f(x) = 2x // I double my input
g(x) = x + 2 // I add two to my input
h(x) = x^2 // I square my input
Enter fullscreen mode Exit fullscreen mode

Then I can compose them together to make one big function:

comp(x) = h(g(f(x))) 
Enter fullscreen mode Exit fullscreen mode

Notice the letters come in reverse order reading left to right, but we work our way from inside out: the output of f becomes the input of g, and the output of g becomes the input of h. So what does comp(x) do to its input?

comp(x) // I double my input (f), then add two (g), then square the result (h).
Enter fullscreen mode Exit fullscreen mode

Swift Composition of Functions

In swift, when we run a network request, we are composing three functions in much the same way.

The first function, f, prepares the request, setting up all the necessary parameters. It sends that information to g over on a new thread, and g does the actual request, the part that takes some time. Then g sends its results to h, the completion handler, back on the main thread, so h can display and/or store the results of the request.

f(my request) = a new network request, and h
g(the new network request, h) = make the request, get the results, then call h
h(results) = store and/or display results
Enter fullscreen mode Exit fullscreen mode

The whole process looks like: h(g(f(x))).

Details

That whole jumble of network code became much more clear to me once I could view it as a composition of functions. Here are a few more details:

The function h itself is passed as an input to g. In Swift, a function passed around like a variable is called a “closure.” The closure h is called a “completion handler,” because it handles the completion of the task. Passing h in as input rather than hard-coding it allows us to use the same network request function (g) for different requests that need to be completed in different ways.

The completion handler (h) also needs to be marked as “escaping” in Swift because while the code for it originates in f’s context, it will actually be called from g’s context.

Note that “escaping” and “closure” were part of that heading in my notes — words I struggled with before the lightbulb went on. My larger point here is that it’s easier to understand the details when you can find an abstract idea like “composition of functions” to hang them on.

I Get How, But Why???

Now that I could see how a callback works, I had to ask why three separate functions? Isn’t there some less convoluted way to accomplish the same thing? In my notes I considered all the alternatives. That little exercise strengthened my understanding of why we do things the way we do, which will also help me retain the details.

Consider this: Couldn’t we just use one function, f?

Hi, I’m f, I’m gonna…
1. prepare a network request
2. perform the network request
3. deal with the results of the network request
Enter fullscreen mode Exit fullscreen mode

You may be able to answer this yourself by now: The one-function approach comes with both an execution dilemma and an efficiency problem.

In step 2., if we let the network request run on the main thread, it will likely cause the user interface to hang up. But if we send the request to a different thread, then step 3 will execute immediately, and fail, trying to handle results that aren’t yet available. (I learned that the hard way!)

This approach would also require us to repeat a lot of code. Every different network request would require a whole new function, even if only a single line of code needed changing.

Okay, but can’t we do this with two functions? There are three possible combinations of two functions that might achieve the desired result. I considered them all, and the same problems crop up. The closest to working is this one:

Hi, I’m f. I’m gonna…
1. set up the network request and 
2. perform the network request, then I’ll pass the results to my buddy h to 
3. handle the results.
Enter fullscreen mode Exit fullscreen mode

This might execute okay, assuming f runs the network request on a separate thread, awaiting the results before calling h back on the main thread. But we still have the efficiency problem. We would end up unnecessarily repeating the same network request code for every different request we might set up.

MVC

Another consideration is that any approach using less than three functions violates the MVC (model, view, control) principal of object-oriented coding. We want fundamentally different tasks to be separated into different functions. This make things not only more efficient, but also easier to read, debug, and update. That’s a claim I’ve read many times in my learning journey, but the first time I really felt it to be true was while I was messing around with these network request functions.

Levels of Abstraction

The learning theme here is about using levels of abstraction: by hanging details onto more basic ideas, you can mentally organize what otherwise might feel like a jumble of loosely related concepts.

While I was still struggling with the network request code, I wrote the following in my notes:

“What’s the difference between completion handler, callback, and closure? Are they the same?”

It turns out, the three terms are not interchangeable, however, they are related by levels of abstraction.

A closure in Swift is a function that gets assigned to a variable or passed directly as a parameter into another function.

A completion handler in Swift is a function, passed as a closure, that gets put on hold, waiting for some other process to complete before it can handle the results.

A callback is a design pattern which can be implemented in any language, in which a function is passed to another thread or context, to be executed back in its original context at a later time, when needed.

In short: a “closure” can be used as a “completion handler” when executing the “callback” design pattern in Swift.

The meanings move from more specific to more abstract.

Thanks for Reading!

Insight often comes in the form of abstraction. Without this mode of thinking, I would still be lost, forever wandering the network-request labyrinth of the coding universe.

Now go see if you can abstract some concepts out of whatever details you’re struggling with at the moment!

. . . . . . . . . . . . . . . . . . . . . . .