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

Exploring Swift 5.2’s new functional features

Published on 09 Feb 2020

On the surface level, Swift 5.2 is definitely a minor release in terms of new language features, as much of the focus of this new release has been on improving the speed and stability of Swift’s underlying infrastructure — such as how compiler errors are reported, and how build-level dependencies are resolved.

However, while Swift 5.2’s total number of new language features might be relatively small, it does include two new capabilities that could potentially have quite a big impact on Swift’s overall power as a functional programming language.

This week, let’s explore those features, and how we could potentially use them to embrace a few different paradigms that are very popular in the functional programming world — in ways that might feel more consistent and familiar within an object-oriented Swift code base.

Calling types as functions

Even though Swift isn’t a strictly functional programming language, there’s no doubt that functions play a very central role in its overall design and usage. From how closures are used as asynchronous callbacks, to how collections make heavy use of classic functional patterns like map and reduce — functions are everywhere.

What’s interesting about Swift 5.2 in this regard is that it starts to blur the lines between functions and types. Although we’ve always been able to pass any given type’s instance methods as functions (since Swift supports first class functions), we’re now able to call certain types as if they were functions themselves.

Let’s start by taking a look at an example using an excerpt of the Cache type that we built in ”Caching in Swift” — which provides a more “Swift-friendly” API on top of a wrapped NSCache:

class Cache<Key: Hashable, Value> {
    private let wrapped = NSCache<WrappedKey, Entry>()
    private let dateProvider: () -> Date
    private let entryLifetime: TimeInterval
    
    ...

    func insert(_ value: Value, forKey key: Key) {
        ...
    }
}

Let’s say that we wanted to add a convenience API to the above type — to let us automatically use an inserted value’s id as its cache key, in case the current Value type conforms to the standard library’s Identifiable protocol. While we could simply name that new API insert as well, we’re going to give it a very particular name — callAsFunction:

extension Cache where Value: Identifiable, Key == Value.ID {
    func callAsFunction(_ value: Value) {
        insert(value, forKey: value.id)
    }
}

That might seem like a strange naming convention, but by naming our new convenience method that way, we’ve actually given our Cache type an interesting new capability — it may now be called as if it was a function — like this:

let document: Document = ...
let cache = Cache<Document.ID, Document>()

// We can now call our 'cache' variable as if it was referencing a
// function or a closure:
cache(document)

That’s arguably both really cool, and really strange. But the question is — what might it be useful for? Let’s continue exploring by taking a look at a DocumentRenderer protocol, that defines a common interface for various types that are used to render Document instances within an app:

protocol DocumentRenderer {
    func render(_ document: Document,
                in context: DocumentRenderingContext,
                enableAnnotations: Bool)
}

Similar to how we previously added a function-based convenience API to our Cache type, let’s do the same thing here — only this time, we’ll extend the above protocol to enable any conforming type to be called as a function with a set of default arguments:

extension DocumentRenderer {
    func callAsFunction(_ document: Document) {
        render(document,
            in: .makeDefaultContext(),
            enableAnnotations: false
        )
    }
}

Each of the above two changes might not seem that impressive in isolation, but if we put them together, we can start to see the appeal of providing function-based convenience APIs for some of our more complex types. For example, here we’ve built a DocumentViewController — which uses both our Cache type, and a Core Animation-based implementation of our DocumentRenderer protocol — both of which can now simply be called as functions when a document was loaded:

class DocumentViewController: UIViewController {
    private let cache: Cache<Document.ID, Document>
    private let render: CoreAnimationDocumentRenderer
    
    ...

    private func documentDidLoad(_ document: Document) {
        cache(document)
        render(document)
    }
}

That’s quite cool, especially if we’re aiming for a more lightweight API design, or if we’re building some form of domain-specific language. While it’s always been possible to achieve a similar result by passing instance methods as if they were closures — by enabling our types to be called directly, we both avoid having to manually pass those methods, and we’re able to retain any external parameter labels that our APIs might be using.

For example, let’s say that we also wanted to make a PriceCalculator become a callable type. To maintain the semantics of our original API, we’ll keep the for external parameter label, even when declaring our callAsFunction implementation — like this:

extension PriceCalculator {
    func callAsFunction(for product: Product) -> Int {
        calculatePrice(for: product)
    }
}

Here’s how the above approach compares to if we were to store a reference to our type’s calculatePrice method instead — note how the first piece of code discards our parameter label, while the second retains it:

// Using a method reference:
let calculatePrice = PriceCalculator().calculatePrice
...
calculatePrice(product)

// Calling our type directly:
let calculatePrice = PriceCalculator()
...
calculatePrice(for: product)

Enabling types to be called as if they were functions is a very intriguing concept, but perhaps even more interesting is that it also enables us to go the opposite direction — and transform functions into proper types.

Functional programming in an object-oriented way

While there’s a tremendous amount of power in many functional programming concepts, applying those concepts and patterns when using heavily object-oriented frameworks (like most of Apple’s are) can often be quite challenging. Let’s see if Swift 5.2’s new callable types feature that can help us change that.

Since we can now make any type callable, we could also enable any function to be converted into a type, while still enabling that function to be called as it normally would. To make that happen, let’s define a type called Function, which looks like this:

struct Function<Input, Output> {
    let raw: (Input) -> Output

    init(_ raw: @escaping (Input) -> Output) {
        self.raw = raw
    }

    func callAsFunction(_ input: Input) -> Output {
        raw(input)
    }
}

Just like the callable types that we defined earlier, Function instances can be called directly, making them act the same way as their underlying functions in most cases.

To enable functions that don’t accept any input to still be called without having to manually specify Void as an argument, let’s also define the following extension for Function values which have Void as their Input type:

extension Function where Input == Void {
    func callAsFunction() -> Output {
        raw(Void())
    }
}

The cool thing about the above wrapper type is that it enables us to adopt really powerful functional programming concepts in much more object-oriented ways. Let’s take a look at two such concepts — partial application and piping (which we also used in “Functional networking in Swift”). The former lets us combine a function with a value to produce a new function that doesn’t require any input, while the latter enables us to chain two functions together — and could now be implemented like this:

extension Function {
    func combined(with value: Input) -> Function<Void, Output> {
        Function<Void, Output> { self.raw(value) }
    }
    
    func chained<T>(to next: @escaping (Output) -> T) -> Function<Input, T> {
        Function<Input, T> { next(self.raw($0)) }
    }
}

Note how we named the above two functions combined and chained in order to make them feel more “at home” in Swift, rather than using the names typically found in more strictly functional programming languages.

What the above setup enables us to do is to use techniques like function-based dependency injection in a way that still feels very object-oriented. For example, here we’ve built a view controller for editing notes — which accepts two functions, one for loading the current version of the note that it’s editing, and one for submitting an update to our app’s central data store:

class NoteEditorViewController: UIViewController {
    private let provideNote: Function<Void, Note>
    private let updateNote: Function<Note, Void>

    init(provideNote: Function<Void, Note>,
         updateNote: Function<Note, Void>) {
        self.provideNote = provideNote
        self.updateNote = updateNote
        super.init(nibName: nil, bundle: nil)
    }
    
    ...

    private func editorTextDidChange(to text: String) {
        var note = provideNote()
        note.text = text
        updateNote(note)
    }
}

The beauty of the above approach is that it lets us build our UI in a way that’s completely decoupled from the concrete types that we use to drive our model and data logic. For example, the functions that our above view controller actually uses are in this case methods on a NoteManager type, that looks like this:

class NoteManager {
    ...

    func loadNote(withID id: Note.ID) -> Note {
        ...
    }
    
    func updateNote(_ note: Note) {
        ...
    }
}

Then, when we’re creating an instance of our view controller, we’re using our Function type to convert the above two methods into functions that our UI code can directly call — without having to be aware of any of the underlying types or details:

func makeEditorViewController(
    forNoteID noteID: Note.ID
) -> UIViewController {
    let provider = Function(noteManager.loadNote).combined(with: noteID)
    let updater = Function(noteManager.updateNote)

    return NoteEditorViewController(
        provideNote: provider,
        updateNote: updater
    )
}

Not only does the above approach give us a greater separation of concerns, it also makes testing a breeze, as we no longer have to mock any protocols or fight with singleton-based global state — we can simply inject any sort of behavior that we wish to test against by passing in test-specific functions.

Passing key paths as functions

Another really interesting new feature introduced in Swift 5.2 is that key paths can now be passed as functions. That comes very much in handy in situations when we’re using a closure simply to extract a piece of data from a property — as we can now pass that property’s key path directly:

let notes: [Note] = ...

// Before:
let titles = notes.map { $0.title }

// After:
let titles = notes.map(\.title)

Combining that capability with our Function type from before, we can now easily construct a chain of functions that lets us load a given value, and then extract a property from it. Here we’re doing just that to create a function that lets us easily look up what tags that are associated with a given note ID:

func tagLoader(forNoteID noteID: Note.ID) -> Function<Void, [Tag]> {
    Function(noteManager.loadNote)
        .combined(with: noteID)
        .chained(to: \.tags)
}

Of course, the above examples barely scratch the surface of what could be possible when we start to mix functional programming patterns with object-oriented APIs — so it’s definitely a topic that we’ll return to in future articles.

Conclusion

Swift 5.2 and Xcode 11.4 are both quite substantial releases — with a new diagnostics engine for compiler errors, lots of new testing and debugging features, and much more. But Swift 5.2 is also an interesting release from a syntax perspective, as it continues to broaden the ways that Swift can be used to adopt functional programming concepts, and how it starts to blur the lines between types and functions.

I’ll of course continue to explore these features over the coming weeks and months, and will report my findings in future articles, podcast episodes and videos.

What do you think? What are your first impressions of these new features, and do you have any concrete use cases in which they might become useful? Let me know — along with your questions, comments or feedback — either via email or Twitter.

Thanks for reading! 🚀