observations about swift

Fri, Jun 21, 2024

I'm currently going through learning the Swift programming language, after finally biting the bullet and realizing that many of my ideas are best executed as mobile apps. The popular perception may be that mobile is dying, but I think LLMs will so wildly change the device productivity calculus that many more people may opt to rely on their phones to run their personal lives, rather than their laptops.

I've been using Go, Rust, C and some such statically typed languages for a long time, especially Go. Swift rose to prominence for iOS at some point after I'd gotten it through my skull that Objective-C was the name of the game, and I confess to ignoring Swift for its entire existence until last week.

So I picked up the official swift documentation and have been working through it. Of particular note (and strong appreciation) is the presence of a so-called tour of the language, almost at the very front of the documentation, designed for developers to transition to Swift. The documentation knows the reader aims to quickly understand Swift with minimum effort, and it does a great job—in fact, it can't help but compare exactly the syntax that the reader may not expect, and little else:

It strikes me as a document that respects the reader far more than average. It's much less self-indulgent than I've read in other language overviews, and better than I expected from afar.

As I read the tour, a few things struck me as particularly interesting choices. In the interest of sharing these, and of solidifying my own knowledge of the language in the process, I share a few.

function declaration

Consider the way to define a function:

By default, functions use their parameter names as labels for their arguments. Write a custom argument label before the parameter name, or write _ to use no argument label.

func greet(_ person: String, on day: String) -> String {

    return "Hello \(person), today is \(day)."

}

greet("John", on: "Wednesday")

Fascinating! You can choose whether to force the user to type the name of each argument, rely fully on positional arguments, or a combination—and you can even decouple the name at the call site from the internal name. I like that a lot. It feels like it does what many IDEs wish to do with positional arguments.

tuples

The language has tuples! You can name each value or work with them positionally. Literally better than Golang, where conventions insists on making the user define entire structs for a single situation in which you need to work with, say, parallel lists.

functions as objects

One choice I'm not thrilled about is the syntax for returning a function from a function:

func makeIncrementer() -> ((Int) -> Int) {

    func addOne(number: Int) -> Int {

        return 1 + number

    }

    return addOne

}

var increment = makeIncrementer()

increment(7)

The problem is on the first line, specifically ((Int) -> Int). It's obviously distinct from a tuple in two ways, the nested parentheses and the arrow operator, but to my mind, it would be much clearer at a glance to return this only with an explicit func keyword like in Go. Eh, maybe I have Stockholm syndrome because this is less verbose, but I feel like I would glance over this sometimes and get confused.

closures

In Go, there's hardly ever a need to use anonymous functions except with goroutines, and even then many people think it's an antipattern. Swift's closure syntax has a rather clunky in keyword that I don't think makes great conceptual sense, at least compared to Rust's excellent vertical-bar syntax for the same. Here's an example:

numbers.map({ (number: Int) -> Int in

    let result = 3 * number

    return result

})

Now we have a compounding problem. No func keyword, but sure, I get the vibe of the curly braces to indicate a closure. But what the hell is "in" what exactly? What mental phrase am I supposed to call upon to make this keyword explain itself? I can confabulate a few, but this is wildly unintuitive.

Closures are redeemed somewhat by the next example, wherein we learn that closures implicitly return their only statement, and you can omit the function signature if the return type is implied by the caller:

let mappedNumbers = numbers.map({ number in 3 * number })

print(mappedNumbers)

// Prints "[60, 57, 21, 36]"

I'll admit though that this level of conciseness blows Golang out of the water.

classes

Here is where my low-level programming experience makes me a curmudgeon: I don't care at all for most of what classes can do in other programming languages. I don't like inheritance; I don't like constructors defined inside the class definition; I don't like any of it.

Here's the example from the docs:

class Square: NamedShape {

    var sideLength: Double


    init(sideLength: Double, name: String) {

        self.sideLength = sideLength

        super.init(name: name)

        numberOfSides = 4

    }


    func area() -> Double {

        return sideLength * sideLength

    }


    override func simpleDescription() -> String {

        return "A square with sides of length \(sideLength)."

    }

}

let test = Square(sideLength: 5.2, name: "my test square")

test.area()

test.simpleDescription()

I know people love this stuff, but this is way too object-oriented for me. We'll see, but I expect that I'll stick with my simple structs and tuples. The only exception might be properties-as-functions, which is quite nice, particularly when you don't want to care about which of two related properties is the actual value and which is the dependent value. Here's a snippet from the EquilateralTriangle example that I like:

var perimeter: Double {

    get {

         return 3.0 * sideLength

    }

    set {

        sideLength = newValue / 3.0

    }

}

I would use this. I would use precisely this. But they had to go further and add not only both a struct and a class keyword, the former of which is always passed by value and the latter by reference (what's that about? Can't you, just... let me indicate when to pass in each way?), but also an actor keyword for concurrency-safe classes, and also willSet and didSet methods which muddy this entirely; now you can have computed properties and properties with potentially invisible side-effects upon other concrete values in a struct! You can even do that in one direction and not the other, opening up the developer to a wide range of forgetfulness mistakes: "oh, when you set A you set B to nil, but not vice-versa? What a silly goose!"

Look, I like minimal and low-level programming languages. That's obvious. But I also have a soft spot for languages like Ruby and Lisp: you either embrace being close to the metal, in control of the details of memory management or whatever, or you smooth the water as much as possible and try to create graceful abstractions, the potential for metaprogramming, whatever. But this type of object-orientation is something that most newer popular languages aren't doing anymore, or are at least doing more carefully than this.

Whenever you introduce a keyword to a language, you're greatly increasing the cognitive overhead for the reader of the code. Language writers should strive to minimize code words and syntactical jargon, even if you want powerful abstractions. Say no to bad ideas!

enums

Swift enums seem great. No complaints. This is the third-biggest problem with Golang (sorry, iota just isn't good enough) and I'm pleased to see proper enums here.

async/await

No no no no no. Unfortunate. Also, why does the async keyword go after the arguments and before the return type? Is it the method that's asynchronous or the return value, here???

protocols and extensions

We have now treaded so far away from sense that I am concerned. By my count, we now have five types of first-class objects, all with subtly different properties that can't be intuited from their names. Extensions are at least somewhat clear. Kinda cool to be able to add properties to any type you want, even the primitives... but I am not sure I will ever do this. The protocol keyword is a welcome addition, but perhaps would've been better named an interface as one would more strongly expect in other languages. Perhaps over time I will discover a reason for the odd name.

error

Try-catch ain't it, chief. Golang may not do a great job of this either, but Swift somehow strikes the worst balance I've ever seen in a supposedly modern language: you have to specify which functions are able to throw errors with the throws keyword in the signature (great!) but you don't have to specify which errors it can throw! Let me also leave this example here to indicate how gross this can get whenever you're a few layers deep in network calls or whatever:

do {

    let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")

    print(printerResponse)

} catch PrinterError.onFire {

    print("I'll just put this over here, with the rest of the fire.")

} catch let printerError as PrinterError {

    print("Printer error: \(printerError).")

} catch {

    print(error)

}

// Prints "Job sent"

What a beautiful day it was when I realized Go didn't have this, even though Go errors have their own problems (namely a lack of specificity, indeed, without breaking from convention...). We're not Python here, but this is pretty bad.

It is at least possible to use try? to disregard the specific error and return an optional instead, which would be a lot better if I could at least get a string or some nice way of logging the output in the event an error is thrown, but it's better than forcing a try-catch at the call site every time.

concluding thoughts

All this is what it is. Swift intimidates me because Apple development is such a rabbit hole of domain knowledge: one must know the specific libraries (SwiftUI, CloudKit, whatever else) inside and out to be efficient and effective. I'm glad Swift exists over Objective-C, and I'm glad that it's sufficiently general-purpose to be used for things other than Apple development (although it probably never will be). In comparison to the libraries, though, nothing the language does is all that important. People can work through any language: it's the APIs, library quality, aggressive background task management, etc. of the Apple world that occupies the time of the devs I know. I'm sure I'll be no different in that area.

The details aren't my cup of tea, but the strong standardization of the hardware (and, for that matter, the libraries and frameworks I have to learn) means that there's no better time to come into it for the first time. Much has been made easier since development for the iPhone began, and my ambitions aren't that lofty. We'll see how it unfolds.