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:
- Here's how
switch
statements work! - The loop types are
for
,while
andrepeat-while
. nil
is handled with options, and those look like this.
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.