Articles, podcasts and news about Swift development, by John Sundell.

Functional networking in Swift

Published on 06 Jan 2019
Basics article available: Networking

Networking is a particularly interesting topic when it comes to app development. On one hand, it's something that most apps need — and on the other hand it's something that can be really tricky to get right, especially when we want the resulting code to be both easy to read and write, as well as test.

It's therefore not that surprising that networking is one of those areas that both has been a source of great debate in the community, and also something that many different third party frameworks have aimed to make easier.

This week, let's take a look at a take on writing networking code that utilizes Apple's built-in URLSession API — but augments it using both Futures & Promises, as well as several functional programming concepts — to end up with a quite interesting (and highly testable) result. Let's dive in!

Futures & Promises

It seems like Futures & Promises are becoming a more and more popular abstraction when it comes to asynchronous programming in Swift, and for good reason — instead of having to pass multiple closures around (or construct a giant "pyramid" of them), Futures & Promises provide a lightweight way to model an asynchronous result using dedicated objects — that can be returned from functions, chained, transformed and observed.

If you're completely unfamiliar with Futures & Promises, I recommend reading "Under the hood of Futures & Promises in Swift" before continuing with this article.

Here's how we could extend URLSession to support Futures & Promises — while also borrowing the Endpoint type from "Constructing URLs in Swift":

extension URLSession {
    func request(_ endpoint: Endpoint) -> Future<Data> {
        // Start by constructing a Promise, that will later be
        // returned as a Future    
        let promise = Promise<Data>()

        // Immediately reject the promise in case the passed
        // endpoint can't be converted into a valid URL
        guard let url = endpoint.url else {
            promise.reject(with: Endpoint.Error.invalidURL)
            return promise
        }

        let task = dataTask(with: url) { data, _, error in
            // Reject or resolve the promise, depending on the result
            if let error = error {
                promise.reject(with: error)
            } else {
                promise.resolve(with: data ?? Data())
            }
        }

        task.resume()

        return promise
    }
}

One big advantage of Futures & Promises is that they allow us to easily transform an asynchronous result, by actually calling methods on a type rather than having to use nested closures. So to enable a result from our above networking API to be easily decoded, we could simply add an extension on Future when its Value type is Data — like this:

extension Future where Value == Data {
    func decoded<T: Decodable>() -> Future<T> {
        return decoded(as: T.self, using: JSONDecoder())
    }
}

Combining the above two APIs, we're now able to use the power of Futures & Promises to perform network calls, while relying on Swift's type inference to have the downloaded data decoded into the correct type. For example, if our app deals with products, we could now implement our product loading code like this:

class ProductLoader {
    private let urlSession: URLSession

    init(urlSession: URLSession = .shared) {
        self.urlSession = urlSession
    }

    func loadProduct(withID id: Product.ID) -> Future<Product> {
        let endpoint = Endpoint.product(withID: id)
        return urlSession.request(endpoint).decoded()
    }
}

Above we use a very standard way of doing dependency injection, by injecting the URLSession we wish to use when initializing an instance of ProductLoader (we also use .shared as a default argument to make our API nice and easy to use, while still giving us the flexibility needed for things like testing).

A simple signature

While using types for things like loading models, like we do with our ProductLoader above, can be a great solution when more complex loading logic is needed — it can feel a bit heavy when all we want to do is to perform a single network call and decode the result.

If we think about it, essentially our product loading code above can really be expressed as a single function, that takes a product ID and returns a future:

typealias ProductLoading = (Product.ID) -> Future<Product>

Wouldn't it be cool if our networking code could actually look like that? A simple function that could easily be called — without having to instantiate any classes or other types? Let's see if we can work our way there, using a series of steps that involves introducing a number of functional programming concepts into our networking code.

A big part of functional programming is reasoning about a program's logic through the lens of functions — and by matching functions that can be linked together by their input and output. To start looking at our networking code that way, let's first go back to the request method that we initially added to URLSession, and take a look at its signature:

extension URLSession {
    func request(_ endpoint: Endpoint) -> Future<Data> {
        ...
    }
}

Regardless of what happens inside of that method, to the outside world — this is what that function looks like:

typealias Networking = (Endpoint) -> Future<Data>

If we could simply define our networking code as a function that goes from Endpoint to Future<Data>, that'd actually be quite powerful. For starters, instead of having ProductLoader keep a reference to the current URLSession, we could simply let it accept any function that matches the signature of Networking — like this:

class ProductLoader {
    private let networking: Networking

    init(networking: @escaping Networking = URLSession.shared.request) {
        self.networking = networking
    }

    func loadProduct(withID id: Product.ID) -> Future<Product> {
        let endpoint = Endpoint.product(withID: id)
        return networking(endpoint).decoded()
    }
}

Again we use a default argument (this time by passing the request function itself) to make our API easier to use.

The above change is already a pretty great win, since we've essentially removed the hard dependency that ProductLoader previously had on URLSession — which both makes testing easier and helps future-proof our code (if we wish to adopt another networking framework in the future, all we'd have to do is change the default argument).

But we're just getting started 😉.

Combining functions and values

The power of Swift's first class function capabilities is that it lets us pass functions around as if they were closures — or any other value. Just like how we passed URLSession.shared.request as an argument above, we can do the same with other functions as well.

For example, let's say we wanted to create a more specialized version of Networking that could only be used to load a single product — but would enable us to do so without having to supply any product ID at the call site. That would require us to combine our networking function with the product ID in question — to form an entirely new function.

To be able to easily do that in general, let's start by creating a combine function, that takes a value and inlines it into a given function — resulting in a new function that can be called without any arguments, like this:

// This turns an (A) -> B function into a () -> B function,
// by using a constant value for A.
func combine<A, B>(_ value: A,
                   with closure: @escaping (A) -> B) -> () -> B {
    return { closure(value) }
}

In the functional programming world, the above technique is known as partial application, and is also implemented in certain languages (like C++) as bind. In our case, what it now enables us to do is to combine our product Endpoint with our Networking function to form a specialized version of it that can be called directly:

func loadProduct(withID id: Product.ID) -> Future<Product> {
    let endpoint = Endpoint.product(withID: id)
    let networking = combine(endpoint, with: self.networking)

    // Our new networking function can now be called without
    // having to supply a product ID at the call site.
    return networking().decoded()
}

In isolation, the above change might not look that impressive — after all we've actually added more code to seemingly do the same thing 🤔. But let's keep pulling this thread, and see where we'll end up.

A functional chain

Another important concept in functional programming is function composition — the idea that multiple functions can be combined to form a new one.

Similar to how using techniques like child view controllers can enable us to compose our UI from multiple, smaller, building blocks — using function composition enables us to keep the various pieces of our logic isolated in small functions, while still enabling them to be used as one unit at the call site.

One way of composing functions is to simply chain two of them together. The output of the first function (commonly referred to as the inner function), is passed directly as an argument to the second (or outer) function. To enable that sort of composition, let's create another helper function — similar to combine — that lets us do just that:

// This turns an (A) -> B and a (B) -> C function into a
// (A) -> C function, by chaining them together.
func chain<A, B, C>(_ inner: @escaping (A) -> B,
                    to outer: @escaping (B) -> C) -> (A) -> C {
    return { outer(inner($0)) }
}

Using the above chain function, we can now take our static Endpoint.product method, and chain it to our Networking function, resulting in another specialized version that can be called using only a product ID — since our chain will convert that ID into an endpoint and then in turn pass that endpoint to our networking function:

func loadProduct(withID id: Product.ID) -> Future<Product> {
    let networking = chain(Endpoint.product, to: self.networking)
    return networking(id).decoded()
}

Again, perhaps not super useful in isolation — but now we're starting to get somewhere! 😀

Currying

The final functional programming concept that we'll introduce into our networking code is currying — when the result of a function call is yet another function, which is then directly called. In Swift, currying is actually more common than what it first might seem like — in fact all instance methods are actually implemented by currying static methods under the hood.

For example, let's say that we have a Letter class that has an open method:

class Letter {
    func open() {
        ...
    }
}

The "normal" way to call the above method would be to do something like this:

let letter = Letter()
letter.open()

But an equally correct way would be to use currying — by calling the open method's static equivalent, passing in the instance we wish to obtain a function for, and then directly call it once returned — like this:

let letter = Letter()
Letter.open(letter)()

That might not seem particularly useful, but using the above technique will actually enable us to also compose instance methods without having to know the instance in advance — which is pretty cool.

Let's start by creating another overload of chain that enables us to pass a curried function as the second argument. Since such functions return another function as their result, our new overload will look like this:

// This turns an (A) -> B and a (B) -> () -> C function into a
// (A) -> C function, by chaining them together.
func chain<A, B, C>(
    _ inner: @escaping (A) -> B,
    to outer: @escaping (B) -> () -> C
) -> (A) -> C {
    // Similar to our previous version of chain, we pass the result
    // of the inner function into the outer one — but since that
    // now returns another function, we'll also call that one.
    return { outer(inner($0))() }
}

Now that we're able to also chain instance methods without first having to know what instance they will be applied to — we can easily add the decoded method from our Future extension into our chain, resulting in a single function that wraps up everything that we need to load a product with a given ID:

func loadProduct(withID id: Product.ID) -> Future<Product> {
    let networking = chain(Endpoint.product, to: self.networking)
    return chain(networking, to: Future.decoded)(id)
}

Having both of the above kinds of chaining at our disposal is incredibly powerful — and lets us work with functions in whole new ways — especially when we start putting everything we've done up to this point together.

A functional composition

At first it might seem like using functional concepts like we've done so far is a purely theoretical exercise that doesn't have much practical value — and that might be true when those concepts are used in isolation, but once they're combined, we're able to do some really powerful things.

First of all — the initial idea of reducing our product loading code into a single function is now only a matter of using both of our chain overloads to compose the individual functions we need into one that can be used directly — without the need for any ProductLoader type:

extension URLSession {
    var productNetworking: (Product.ID) -> Future<Product> {
        let networking = chain(Endpoint.product, to: request)
        return chain(networking, to: Future.decoded)
    }
}

What that in turn enables us to do is to completely remove the awareness of networking from a large part of our code base — since in order to load a model or perform another kind of network-bound operation, we now only need a function matching the required signature — we don't need to depend on any concrete types.

For example, here's how a ProductViewController could have its underlying networking completely abstracted away — and simply accept a function that returns a Future<Product>:

class ProductViewController: UIViewController {
    typealias Loading = () -> Future<Product>

    private let loading: Loading

    init(loading: @escaping Loading) {
        self.loading = loading
        super.init(nibName: nil, bundle: nil)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        // To load the view controller's product, we now
        // simply have to call the injected closure, and
        // observe the returned future.
        loading().observe { [weak self] result in
            switch result {
            case .value(let product):
                self?.render(product)
            case .error(let error):
                self?.handle(error)
            }
        }
    }
}

When following the above approach, constructing view controllers also becomes super simple — since all we have to do is to use our combine function to create the right kind of loading function, by combining the data we're basing the request on with the underlying networking function:

func makeProductViewController(forID id: Product.ID) -> UIViewController {
    let networking = combine(id, with: URLSession.shared.productNetworking)
    return ProductViewController(loading: networking)
}

Finally — and perhaps best of all — is that testing code that uses a functional approach to networking becomes trivial. Since our view controllers (and other code that relies on network calls) no longer have any dependency on our actual networking code, all we have to do when writing tests is to pass in functions that return the values that we're basing our tests on:

let viewController = ProductViewController {
    let product = Product(id: 7, name: "iPad Pro")
    return Promise(value: product)
}

For more on this kind of mock-free testing - check out "Mock-free unit tests in Swift".

Not only have we reduced the number of types that we need to maintain, we've also enabled testability without the need for any protocols or additional infrastructure — and since all we're doing is composing functions — our code should remain highly reusable as well. Pretty cool! 😎

Conclusion

Functional programming continues to be an area of great interest for me and many others in the Swift community. While there are a number of ways that the solutions in this article could be taken further — for example by using common operators for things like function composition and forward application — using functional programming concepts as inspiration, and then implementing our own "flavor" of them, can provide a great middle ground between the object-oriented world of Apple's SDKs and a "pure" functional programming world.

I'm sure we'll continue exploring functional programming more in future articles. Like with most things, it's important to find the right balance between various concepts for any given project — and to carefully consider what concepts to introduce into a code base. But for networking in particular — many functional concepts prove to be quite a good fit, especially since we're often dealing with pure transformations over data.

What do you think? Do you currently use some of these functional programming concepts in your networking code, or is it something you'll try out? Let me know — along with your questions, comments and feedback — on Twitter @johnsundell.

Thanks for reading! 🚀