Static and Dynamic Callable Types in Swift

Last week, Apple released the first beta of Xcode 11.4, and it’s proving to be one of the most substantial updates in recent memory. XCTest got a huge boost, with numerous quality of life improvements, and Simulator, likewise, got a solid dose of TLC. But it’s the changes to Swift that are getting the lion’s share of attention.

In Xcode 11.4, Swift compile times are down across the board, with many developers reporting improvements of 10 – 20% in their projects. And thanks to a new diagnostics architecture, error messages from the compiler are consistently more helpful. This is also the first version of Xcode to ship with the new sourcekit-lsp server, which serves to empower editors like VSCode to work with Swift in a more meaningful way.

Yet, despite all of these improvements (which are truly an incredible achievement by Apple’s Developer Tools team), much of the early feedback has focused on the most visible additions to Swift 5.2. And the response from the peanut galleries of Twitter, Hacker News, and Reddit has been — to put it charitably — “mixed”.


If like most of us, you aren’t tuned into the comings-and-goings of Swift Evolution, Xcode 11.4 was your first exposure to two new additions to the language: key path expressions as functions and callable values of user-defined nominal types.

The first of these allows key paths to replace one-off closures used by functions like map:

// Swift >= 5.2
"🧁🍭🍦".unicodeScalars.map(\.properties.name)
// ["CUPCAKE", "LOLLIPOP", "SOFT ICE CREAM"]

// Swift <5.2 equivalent
"🧁🍭🍦".unicodeScalars.map { $0.properties.name }

The second allows instances of types with a method named callAsFunction to be called as if they were a function:

struct Sweetener {
    let additives: Set<Character>

    init<S>(_ sequence: S) where S: Sequence, S.Element == Character {
        self.additives = Set(sequence)
    }

    func callAsFunction(_ message: String) -> String {
        message.split(separator: " ")
               .flatMap { [$0, "\(additives.randomElement()!)"] }
               .joined(separator: " ") + "😋"
    }
}

let dessertify = Sweetener("🧁🍭🍦")
dessertify("Hello, world!")
// "Hello, 🍭 world! 🍦😋"

Granted, both of those examples are terrible. And that’s kinda the problem.


Too often, coverage of “What’s New In Swift” amounts to little more than a regurgitation of Swift Evolution proposals, interspersed with poorly motivated (and often emoji-laden) examples. Such treatments provide a poor characterization of Swift language features, and — in the case of Swift 5.2 — serves to feed into the popular critique that these are frivolous additions — mere syntactic sugar.

This week, we hope to reach the ooey gooey center of the issue by providing some historical and theoretical context for understanding these new features.

Syntactic Sugar in Swift

If you’re salty about “key path as function” being too sugary, recall that the status quo isn’t without a sweet tooth. Consider our saccharine example from before:

"🧁🍭🍦".unicodeScalars.map { $0.properties.name }

That expression relies on at least four different syntactic concessions:

  1. Trailing closure syntax, which allows a final closure argument label of a function to be omitted
  2. Anonymous closure arguments, which allow arguments in closures to be used positionally ($0, $1, …) without binding to a named variable.
  3. Inferred parameter and return value types
  4. Implicit return from single-expression closures

If you wanted to cut sugar out of your diet completely, you’d best get Mavis Beacon on the line, because you’ll be doing a lot more typing.

"🧁🍭🍦".unicodeScalars.map(transform: { (unicodeScalar: Unicode.Scalar) -> String in
    return unicodeScalar.properties.name
})

In fact, as we’ll see in the examples to come, Swift is a marshmallow world in the winter, syntactically speaking. From initializers and method calls to optionals and method chaining, nearly everything about Swift could be described as a cotton candy melody — it really just depends on where you draw the line between “language feature” and “syntactic sugar”.


To understand why, you have to understand how we got here in the first place, which requires a bit of history, math, and computer science. Get ready to eat your vegetables 🥦.

The λ-Calculus and Speculative Computer Science Fiction

All programming languages can be seen as various attempts to represent the λ-calculus. Everything you need to write code — variables, binding, application — it’s all in there, buried under a mass of Greek letters and mathematical notation.

Setting aside syntactic differences, each programming language can be understood by its combination of affordances for making programs easier to write and easier to read. Language features like objects, classes, modules, optionals, literals, and generics are all just abstractions built on top of the λ-calculus.

Any other deviation from pure mathematical formalism can be ascribed to real-world constraints, such as a typewriter from the 1870s, a punch card from the 1920s, a computer architecture from the 1940s, or a character encoding from the 1960s.

Among the earliest programming languages were Lisp, ALGOL*, and COBOL, from which nearly every other language derives.

(defun square (x)
    (* x x))

(print (square 4)) 
;; 16

Here you get a glimpse into three very different timelines; ours is the reality in which ALGOL’s syntax (option #2) “won out” over the alternatives. From ALGOL 60, you can draw a straight line from CPL in 1963, to BCPL in 1967 and C in 1972, followed by Objective-C in 1984 and Swift in 2014. That’s the lineage that informs what types are callable and how we call them.


Now, back to Swift…

Function Types in Swift

Functions are first-class objects in Swift, meaning that they can be assigned to variables, stored in properties, and passed as arguments or returned as values from other functions.

What distinguishes function types from other values is that they’re callable, meaning that you can invoke them to produce new values.

Closures

Swift’s fundamental function type is the closure, a self-contained unit of functionality.

let square: (Int) -> Int = { x in x * x }

As a function type, you can call a closure by passing the requisite number of arguments between opening and closing parentheses ()a la ALGOL.

square(4) // 16

Closures are so called because they close over and capture references to any variables from the context in which they’re defined. However, capturing semantics aren’t always desirable, which is why Swift provides dedicated syntax to a special kind of closure known as a function.

Functions

Functions defined at a top-level / global scope are named closures that don’t capture any values. In Swift, you declare them with the func keyword:

func square(_ x: Int) -> Int { x * x }
square(4) // 16

Compared to closures, functions have greater flexibility in how arguments are passed.

Function arguments can have named labels instead of a closure’s unlabeled, positional arguments — which goes a long way to clarify the effect of code at its call site:

func deposit(amount: Decimal,
             from source: Account,
             to destination: Account) throws {  }
try deposit(amount: 1000.00, from: checking, to: savings)

Functions can be generic, allowing them to be used for multiple types of arguments:

func square<T: Numeric>(_ x: T) -> T { x * x }
func increment<T: Numeric>(_ x: T) -> T { x + 1 }
func compose<T>(_ f: @escaping (T) -> T, _ g: @escaping (T) -> T) -> (T) -> T {
    { x in g(f(x)) }
}

compose(increment, square)(4 as Int) // 25 ((4 + 1)²)
compose(increment, square)(4.2 as Double) // 27.04 ((4.2 + 1)²)

Functions can also take variadic arguments, implicit closures, and default argument values (allowing for magic expression literals like #file and #line):

func print(items: Any...) {  }

func assert(_ condition: @autoclosure () -> Bool,
            _ message: @autoclosure () -> String = String(),
            file: StaticString = #file,
            line: UInt = #line) {  }

And yet, despite all of this flexibility for accepting arguments, most functions you’ll encounter operate on an implicit self argument. These functions are called methods.

Methods

A method is a function contained by a type. Methods automatically provide access to self, allowing them to effectively capture the instance on which they’re called as an implicit argument.

struct Queue<Element> {
    private var elements: [Element] = []

    mutating func push(_ newElement: Element) {
        self.elements.append(newElement)
    }

    mutating func pop() -> Element? {
        guard !self.elements.isEmpty else { return nil }
        return self.elements.removeFirst()
    }
}

Putting everything together, these syntactic affordances allow Swift code to be expressive, clear, and concise:

var queue = Queue<Int>()
queue.push(1)
queue.push(2)
queue.pop() // 1

Compared to more verbose languages like Objective-C, the experience of writing Swift is, well, pretty sweet. It’s hard to imagine any Swift developers objecting to what we have here as being “sugar-coated”.

But like a 16oz can of Surge, the sugar content of something is often surprising. Turns out, that example from before is far from innocent:

var queue = Queue<Int>() // desugars to `Queue<Int>.init()`
queue.push(1) // desugars to `Queue.push(&queue)(1)`

All this time, our so-called “direct” calls to methods and initializers were actually shorthand for function currying partially-applied functions.

With this in mind, let’s now take another look at callable types in Swift more generally.

{Type, Instance, Member} ⨯ {Static, Dynamic}

Since their introduction in Swift 4.2 and Swift 5, respectively, many developers have had a hard time keeping @dynamicMemberLookup and @dynamicCallable straight in their minds — made even more difficult by the introduction of callAsFunction in Swift 5.2.

If you’re also confused, we think the following table can help clear things up:

  Static Dynamic
Type init N/A
Instance callAsFunction @dynamicCallable
Member func @dynamicMemberLookup

Swift has always had static callable types and type members. What’s changed in new versions of Swift is that instances are now callable, and both instances and members can now be called dynamically.

Let’s see what that means in practice, starting with static callables.

Static Callable

struct Static {
    init() {}

    func callAsFunction() {}

    static func function() {}
    func function() {}
}

This type can be called statically in the following ways:

let instance = Static() //  desugars to `Static.init()`

Static.function() //  (no syntactic sugar!)
instance.function() //  desugars to Static.function(instance)()

instance() //  desugars to `Static.callAsFunction(instance)()`
Calling the Static type invokes an initializer
Calling function on the Static type invokes the corresponding static function member, passing Static as an implicit self argument.
Calling function on an instance of Static invokes the corresponding function member, passing the instance as an implicit self argument.
Calling an instance of Static invokes the callAsFunction() function member, passing the instance as an implicit self argument.

Dynamic Callable

@dynamicCallable
@dynamicMemberLookup
struct Dynamic {
    func dynamicallyCall(withArguments args: [Int]) -> Void { () }
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Void { () }

    static subscript(dynamicMember member: String) -> (Int) -> Void { { _ in } }
    subscript(dynamicMember member: String) -> (Int) -> Void { { _ in } }
}

This type can be called dynamically in a few different ways:

let instance = Dynamic() // desugars to `Dynamic.init()`

instance(1) //  desugars to `Dynamic.dynamicallyCall(instance)(withArguments: [1])`
instance(a: 1) //  desugars to `Dynamic.dynamicallyCall(instance)(withKeywordArguments: ["a": 1])`

Dynamic.function(1) //  desugars to `Dynamic[dynamicMember: "function"](1)`
instance.function(1) //  desugars to `instance[dynamicMember: "function"](1)`
Calling an instance of Dynamic invokes the dynamicallyCall(withArguments:) method, passing an array of arguments and Dynamic as an implicit self argument.
Calling an instance of Dynamic with at least one labeled argument invokes the dynamicallyCall(withKeywordArguments:) method, passing the arguments in a KeyValuePairs object and Dynamic as an implicit self argument.
Calling function on the Dynamic type invokes the static dynamicMember subscript, passing "function" as the key; here, we call the returned anonymous closure.
Calling function on an instance of Dynamic invokes the dynamicMember subscript, passing "function" as the key; here, we call the returned anonymous closure.

Dynamism by Declaration Attributes

@dynamicCallable and @dynamicMemberLookup are declaration attributes, which means that they can’t be applied to existing declarations through an extension.

So you can’t, for example, spice up Int with Ruby-ish natural language accessors:

@dynamicMemberLookup // ⚠︎ Error: '@dynamicMemberLookup' attribute cannot be applied to this declaration
extension Int {
    static subscript(dynamicMember member: String) -> Int? {
        let string = member.replacingOccurrences(of: "_", with: "-")

        let formatter = NumberFormatter()
        formatter.numberStyle = .spellOut
        return formatter.number(from: string)?.intValue
    }
}

// ⚠︎ Error: Just to be super clear, this doesn't work
Int.forty_two // 42 (hypothetically, if we could apply `@dynamicMemberLookup` in an extension)

Contrast this with callAsFunction, which can be added to any type in an extension.


There’s much more to talk about with @dynamicMemberLookup, @dynamicCallable, and callAsFunction, and we look forward to covering them all in more detail in future articles.


But speaking of RubyPython

Swift ⨯ _______

Adding to our list of “What code is like”:

Code is like fan fiction.

Sometimes to ship software, you need to pair up and “ship” different technologies.

In building these features, the “powers that be” have ordained that Swift replace Python for Machine Learning. Taking for granted that an incremental approach is best, the way to make that happen is to allow Swift to interoperate with Python as seamlessly as it does with Objective-C. And since Swift 4.2, we’ve been getting pretty close.

import Python

let numpy = Python.import("numpy")
let zeros = numpy.ones([2, 4])
/* [[1, 1, 1, 1]
    [1, 1, 1, 1]] */

The Externalities of Dynamism

The promise of additive changes is that they don’t change anything if you don’t want them to. You can continue to write Swift code remaining totally ignorant of the features described in this article (most of us have so far). But let’s be clear: there are no cost-free abstractions.

Economics uses the term negative externalities to describe indirect costs incurred by a decision. Although you don’t pay for these features unless you use them, we all shoulder the burden of a more complex language that’s more difficult to teach, learn, document, and reason about.


A lot of us who have been with Swift from the beginning have grown weary of Swift Evolution. And for those on the outside looking in, it’s unfathomable that we’re wasting time on inconsequential “sugar” like this instead of features that will really move the needle, like async / await.

In isolation, each of these proposals is thoughtful and useful — genuinely. We’ve already had occasion to use a few of them. But it can be really hard to judge things on their own technical merits when they’re steeped in emotional baggage.

Everyone has their own sugar tolerance, and it’s often informed by what they’re accustomed to. Being cognizant of the drawbridge effect, I honestly can’t tell if I’m out of touch, or if it’s the children who are wrong

NSMutableHipster

Questions? Corrections? Issues and pull requests are always welcome.

This article uses Swift version 5.2. Find status information for all articles on the status page.

Written by Mattt
Mattt

Mattt (@mattt) is a writer and developer in Portland, Oregon.

Next Article

Software development best practices prescribe strict separation of configuration from code. Learn how you can use xcconfig files to make your Xcode projects more compact, comprehensible, and powerful.