Rust and Swift (viii)

Functions, closures, and an awful lot of Swift syntax.

I am reading through the Swift book, and comparing it to Rust, which I have also been learning over the past few months. As with the other posts in this series, these are off-the-cuff impressions, which may be inaccurate in various ways. I’d be happy to hear feedback! Note, too, that my preferences are just that: preferences. Your tastes may differ from mine. (See all parts in the series.)

Rust and Swift handle function definition fairly similarly, at least for basic function definitions. In fact, for most basic functions, the only difference between the two is the keyword used to indicate that you’re declaring a function: fn in Rust and func in Swift.

Likewise, both return an empty tuple, (), called the unit type in Rust or Void in Swift. Note, however, that this unit/Void type is not like C(++)’s void or Java’s null: you cannot coerce other types to it; it really is an empty tuple.

Type declarations on functions are basically identical for simple cases, though they vary into the details as you get into generics and constraints in generics.

I have no idea why the Swift team chooses to represent function names like this: function_name(_:second_param:third_param:<etc.>). Perhaps it’s a convention from other languages I’m simply unfamiliar with, but it seems both odd and unhelpful: eliding the first parameter name obscures important information. Also, why use colons for the delimiter?

Edit: I’m informed via Twitter and App.net that this reflects how function names work in Objective C, and derives ultimately from Smalltalk.

Being able to name the items in a returned type in Swift is quite handy; it’s something I have often wanted and had to work around with dictionaries or other similar types in Python.

We’ll see how I feel once I’ve been writing both for a while, but initially I strongly prefer Rust’s more-obvious (if also somewhat longer) -> Option<i32> to return an optional integer to Swift’s -> Int?. I am quite confident that I’ll miss that trailing ? somewhere along the way.

I’m sure there’s a reason for Swift’s internal and external parameter names and the rules about using _ to elide the need to use keyword arguments (but automatically eliding the first one) and so on… but I really can’t see the utility, overall. It seems like it would be better just to have Python-like args and keyword args.

That’s doubly so given that Swift’s rules for default-valued parameters map exactly to Python’s: they need to go at the end, after any parameters which don’t have default values.

Swift’s variadic parameters are nice—though of course limited, since if you have more than one, the compiler may not know how to resolve which destination parameter a given argument belongs with. (I imagine the compiler could be extended to be able to handle multiple variadic parameters as long as they were all of different types, but that’s probably not worth the work or the potential confusion it would introduce.) In any case, it’s a small nicety that I do wish Rust had.

Swift’s variable parameters are… interesting. I can see the utility, sort of, but (probably from years of habit with C and Python and pass-by-reference types), it’s just not a pattern that makes a lot of sense to me right now. No doubt I’ll get used to them in idiomatic Swift, but while Rust doesn’t have a similar feature, I suspect I won’t miss it.

In/out parameters—that is, mutable pass-by-reference types—are available in both languages. The syntax is very different here, as are the semantics.

Swift has the inout keyword, supplied before a parameter definition:

func adds4ToInput(inout num: Int) {
num += 4;
}

Rust has instead a variation on every other type definition, declaring the type in this case to be a mutable reference:

fn adds_4_to_input(num: &mut i32) {
num += 4;
}

As usual, in other words, Swift opts to use new syntax (in this case, a dedicated keyword) while Rust opts to use the same syntax used everywhere else to denote a mutable reference. In fairness to Swift, though, this is something of a necessity there. From what I’ve seen so far, Swift generally doesn’t (and perhaps can’t?) do pointers or references explicitly (though of course it’s handling lots of things that way under the covers); arguments to functions are a special case, presumably present primarily for interoperability with Objective-C.

Swift’s function type definitions, as used in e.g. function parameter definitions, are quite nice, and reminiscent of Haskell in the best way. Rust’s are pretty similar, and add in its trait usage—because functions types aretraits. Once again, I really appreciate how Rust builds more complicated pieces of functionality on lower-level constructs in the language. (Swift may be doing similar under the covers, but the Swift book doesn’t say.)

Again, though, the downside to Rust’s sophistication is that it sometimes bundles in some complexity. Returning a function in Swift is incredibly straightforward:

If you understand what’s going on under the covers here, this makes sense: Rust normally stack-allocates a function in a scope, and therefore the doubler function goes out of scope entirely when the get_doubler function returns if you don’t heap-allocate it with Box::new.

In both languages, closures and “ordinary” functions are variations on the same underlying functionality (as it should be). In Rust’s case, functions and closures both implement the Fn trait. In Swift’s case, named functions are a special case of closures.

The Swift syntax for a closure is, well, a bit odd to my eye. The basic form is like this (with the same “doubler” functionality as above):

{ (n: Int) -> Int in return n * 2 }

For brevity, this can collapse down to the shorter form with types inferred from context, parentheses dropped and the return keyword inferred from the fact that the closure has only a single expression (note that this wouldn’t be valid unless in a context where the type of n could be inferred):

{ n in n * 2 }

The simplicity here is nice, reminiscent in a good way of closures/lambdas in other languages.1 The fact that it’s a special case is less to my taste.

Rust’s closure syntax is fairly similar to Swift’s brief syntax. More importantly, there’s no special handling for closures’ final expressions. Remember: the final expression of any block is always returned in Rust.

|n| n * 2

If we wanted to fully annotate the types, as in the first Swift example, it would be like so:

|n: i32| -> i32 { n * 2 }

There are even more differences between the two, because of Rust’s ownership notion and the associated need to think about whether a given closure is being borrowed or moved (if the latter, explicitly using the move keyword).

Swift has the notion of shorthand argument names for use with closures.2 The arguments to a closure get the default names $0, $1, etc. This gets you even more brevity, and is quite convenient in cases where closures get used a lot (map, sort, fold, reduce, etc.).

{ $0 * 2 }

If that weren’t enough, Swift will go so far as to simply reuse operators (which are special syntax for functions) as closures. So a closure call could simply be + for a function expecting a closure operating on two numbers; Swift will infer that it needs to map back to the relevant method definition on the appropriate type.

The upside to this is that the code can be incredibly brief, and—once you’re used to it, at least—still fairly clear. The downside to this is yet more syntax for Swift, and the ever-growing list of things to remember and ways to write the same thing I expect will lead to quite a bit of instability as the community sorts out some expectations for what is idiomatic in any given instance.

And if that weren’t enough, there is more than one way to supply the body of a closure to a Swift function that expects it: you can supply a block ({ /* closure body */ }) after the function which expects it. Yes, this can end up looking nearly identical to the form for declaring a function:

someFunctionExpectingAnIntegerClosure() { n * 2 }

But you can also drop the parentheses if that’s the only argument.

someFunctionExpectingAnIntegerClosure { n * 2 }

In terms of the mechanics of closures, and not just the syntax, the one significant difference between Rust and Swift is the same one we’ve seen in general between the two languages: Swift handles the memory issues automatically; Rust makes you be explicit about ownership. That is, as noted above about the closures themselves, in Rust you may have to move ownership to get the expected behavior. Both behave basically like closures in any other language, though; nothing surprising here. Both also automatically copy values, rather than using references, whever it makes sense to do so.

Swift autoclosures allow for lazy evaluation, which is neat, but: yet more syntax! Seriously. But I think all its other closure syntaxes also allow for lazy evaluation. The only reason I can see to have the special attribute (@autoclosure) here is because they added this syntax. And this syntax exists so that you can call functions which take closures as if they don’t take closures, but rather the argument the closure itself takes. But of course, this leads the Swift book to include the following warning:

Note: Overusing autoclosures can make your code hard to understand. The context and function name should make it clear that the evaluation is being deferred.

Yes, care needed indeed. (Or, perhaps, you could just avoid adding more special syntax that leads to unexpected behaviors?)

Remember: there’s still just one way to write and use a closure in Rust.

This takes me back to something I noticed early on in my analysis of the two languages. In Swift, there’s nearly always more than one way to do things. In Rust, there’s usually one way to do things. Swift prefers brevity. Rust prefers to be explicit. In other words, Swift borrows more of its philosophy from Perl; Rust more from Python.

I’m a Python guy, through and through. Perl drives me crazy every time I try to learn it. You could guess (even if you hadn’t already seen) where this lands me between Rust and Swift.

This post is incredibly long, but I blame that on the (frankly incredible) number of variants Swift has on the same concept.