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 andfunc
in Swift. -
Likewise, both return an empty tuple,
()
, called the unit type in Rust orVoid
in Swift. Note, however, that this unit/Void
type is not like C(++)âsvoid
or Javaâsnull
: 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 aretrait
s. 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:
func getDoubler() -> (Int) -> Int { func doubler(number: Int) -> Int { return number * 2 } return doubler } func main() { let doubler = getDoubler() println("\(doubler(14))") // -> 28 }
Doing the same in Rust is a bit harder, becauseâas of the 1.3 stable/1.5 nightly timeframeâit requires you to explicitly heap-allocate the function. Swift just takes care of this for you.
fn get_doubler() -> Box<Fn(i32) -> i32> { fn doubler(number: i32) -> i32 { number * 2 } Box::new(doubler) } fn main() { let doubler = get_doubler(); println!("{:}", doubler(14)); // -> 28 }
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 theget_doubler
function returns if you donât heap-allocate it withBox::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 ofn
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?)
Good grief. Iâm tired now. Thatâs a half-dozen variants on closure syntax in Swift.
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.
- Previous: Pattern matching and the value of expression blocks.
- Next: Sum types (
enum
s) and more on pattern matching.