graydon2
Today Apple introduced a new language called Swift, along with some pretty hot livecoding environment which, while neat, is an area I'm not very expert in so I'm going to ignore. I am certain the livecoding nerds will discuss it elsewhere.
Swift-the-language I've just finished (quickly and lightly) reading the manual for, so I'll suggest you take my commentary with a grain of salt, but I can point out some things that might be of interest to rushed readers who don't see the obvious bits quite so obviously as someone recently working in this space. Several other Rust nerds are also busy examining and commenting.
Overall I mostly agree with Bryan O'Sullivan's tweet:
When I started working on Rust, a lot of the motivation was the pain of losing all the nice ML-isms I enjoyed in my hobby hacking when it came time to work in C++ "for pay". We actually tried to pull a lot of this stuff (algebraic types, pattern matching, options, annotation-light typing) into the failed ES4 project as well, but it sank under its weight.
It's remarkable to me that in the years between, we've seen such a shift in what's considered "normal" new-language tech. F# is shipping on several platforms (whether or not M# ever actually surfaces again); Scala is considered an employable skill; C++11 has lambdas and local type inference at least, if not algebraic types or pattern matching; Rust actually exists now; and now one can rely on similar comforts in the Apple ecosystem. How delightful!
Swift-the-language I've just finished (quickly and lightly) reading the manual for, so I'll suggest you take my commentary with a grain of salt, but I can point out some things that might be of interest to rushed readers who don't see the obvious bits quite so obviously as someone recently working in this space. Several other Rust nerds are also busy examining and commenting.
Logistics
- It is LLVM-based and compiles to native code like many recent languages. It will probably run quite fast and target many devices natively, including client-side where memory use and latency kinda still matters.
- It appears to have "Objective C object model interop" as a hard design constraint, which limits some of the stuff they can safely do.
- It appears to be proprietary, though I hope for its sake this is not true!
- It seems to borrow quite extensively from C# and ... I don't know how to say this politely or modestly ... Rust. Which is flattering if true! Of course I'm biased. Also a language pluralist, and since Rust is a major cobbling-together of things we liked in other languages (ML, C++, C#, Lisp, Ruby, etc.) I don't mean this to be claiming any sort of originality. It's nice to see ideas we liked and were trying to promote spreading elsewhere. I'd love to see some posts from the design team talking about their process, what they liked and disliked in other languages, tried to borrow or adapt or reinvent themselves. Convergent evolution happens a lot in languages (often to funny effect).
Features
- No explicit pointers (but see below re: inout), it relies on the value/reference type dichotomy like C#. This is a half-way step to regaining some of the performance you lose moving to an all-heap system like Java (note: Java is toying with value types now too) without having the cognitive and design load of explicit pointers. This looks like its biggest difference from Rust. It also means that you can't easily (say) take a pointer to the middle of another structure or array. Which is sad, but a reasonable compromise.
- Obviously in keeping with that, no real mechanisms for reasoning about ownership. When you have a reference type it's an ARC pointer into a shared heap (or its weak partner), period. Regions, uniqueness and all the associated performance and also cognitive load from Rust is not present.
- Single inheritance classes with explicit overriding and properties, plus multiple inheritance interfaces ("protocols"). This is the norm these days, very C#-ish again, thought the protocols do not appear to have default methods they can carry with them. Classes are reference types, unlike structs which are value types.
- Protocols get to play double duty as either concrete types (in which case they denote a reference type you can acquire with as from any supporting type) and as type constraints on type parameters in generic code. This is a delightful convenience Rust stumbled into when designing its trait system and I'm glad to see other languages picking it up. I'm sure it has precedent elsewhere.
- Post-hoc adaptation of types to unrelated protocols using extension. I'm not sure what the coherence rules are on this, I can't find them.
- Lambdas appear to use a very similar Ruby-ish block form (right down to the trailing argument form!) that Rust used a year or two ago but has now moved away from (I kinda liked it). Also a cute even-shorter form using numbered variables, like the Clojure #() reader macro.
- Nice normal functional-language-y algebraic types with tuples and sum types, the latter introduced with enum as in Rust. Pattern matching and destructuring binding as you'd expect, nearly identical to Rust except obviously without the wrinkle of trying to bind explicit pointers. Also they avoided all the syntactic ambiguities that wound up in Rust's pattern language, much to my dismay (they get disambiguated during name resolution later).
- Local type inference, tidied up numeric types, nicer literals, no implicit coercions. Yay.
- Script-language-y (and Go-ish) support for dictionary literals. Minor, but will be popular.
- A basic module system without globs, grouped imports or renaming. No visibility control. Re-exporting is supported though, through attributes.
- No macro system as far as I can tell.
- Non-pervasive-NULL, thank goodness. Instead, a copy of the option-type / nullable sugar that shows up in C# and the recent Facebook "Hack" language, including quite a bit of sugar for option chaining, forcing and binding. This is great.
- let and var to differentiate immutable from mutable bindings.
Peculiarities
- Arrays have strange copy-on-extension semantics, but I think are always in the heap, maybe? Strings are also seemingly CoW and heap. I think. It's unclear.
- Unclear how or if you're allowed to implement the iterator protocol yourself.
- Visually confusable .. and ... operators for exclusive and inclusive ranges.
- Parameters can be inout and such arguments require the seemingly invincible unary& operator! Funny, the inout keyword (also in Objc) refuses to die! In Rust, before we grew a first class region pointer ("lifetime") system, we did this sort of thing also ("parameter modes"). The asymmetry between argument-passing modes and "real" pointers eventually became too much to bear, but I guess Swift is betting it won't be so bad.
- Seemingly not an expression language. I guess "no macro system" so...
- No discussion of error handling aside from the algebraic types, option types, and mysterious but occasional reference to "runtime error". Not sure what the isolation / recovery system is, or if there is one. The word "unwind" doesn't occur in the manual.
- An approach to named parameters that looks suspiciously like the "Olabl" variant of Ocaml. I am not sure how much use this is likely to get!
- Checked arithmetic by default, and hex floating point literals. Yay!
Overall I mostly agree with Bryan O'Sullivan's tweet:
The academic functional language conferences this year are going to feel like a series of victory laps. Good times, people.
— Bryan O'Sullivan (@bos31337) June 2, 2014
When I started working on Rust, a lot of the motivation was the pain of losing all the nice ML-isms I enjoyed in my hobby hacking when it came time to work in C++ "for pay". We actually tried to pull a lot of this stuff (algebraic types, pattern matching, options, annotation-light typing) into the failed ES4 project as well, but it sank under its weight.
It's remarkable to me that in the years between, we've seen such a shift in what's considered "normal" new-language tech. F# is shipping on several platforms (whether or not M# ever actually surfaces again); Scala is considered an employable skill; C++11 has lambdas and local type inference at least, if not algebraic types or pattern matching; Rust actually exists now; and now one can rely on similar comforts in the Apple ecosystem. How delightful!
confirmed
Date: 2014-06-03 10:27 pm (UTC)http://nondot.org/sabre/
HT @BrendanEich (https://twitter.com/BrendanEich/status/473943219487514625)
Re: confirmed
Date: 2014-06-03 10:37 pm (UTC)Re: confirmed
Date: 2015-06-30 01:16 am (UTC)no subject
Date: 2014-06-10 06:29 am (UTC)The double-duty of protocols really is quite nice, but to be honest, it would have been hard to avoid it! We already had protocols-as-existentials in Objective-C, where it's an increasingly common design pattern; and of course we were familiar with protocols-as-constraints from SML signatures, Haskell type classes, C++ concepts, and any number of other languages. It would take a lot of stubbornness to maintain two different language features for all that.
A number of your complaints are things we're hoping to address later, for various definitions of "later" — but unfortunately, I think being any more specific than that would run afoul of our marching orders not to publicly speculate. I will say that a lot of us individually are quite committed to open source!
no subject
Date: 2014-06-10 06:03 pm (UTC)Yeah, on reflection I think I considered the dual-nature (existentials and bounds) of Rust traits surprising and pleasing only because it represented the static/dynamic dispatch boundary strictly in Rust; foo<T:Bar>(v: &T) will compile foo separately, specialized for T, whereas foo(v: &Bar) will not, and will use a vtable for v. In other languages where this pattern has, of course, has existed for some time (eg. Java generics) it's just a way of preserving / returning concrete type names in a context of ubiquitous subtyping; the erasure / boxing compilation model doesn't really change the code being produced (much? at all?)
I can't quite tell at a glance what Swift does here, in terms of compilation model: if you write a (bounded) parametric function, does it get multiply compiled and specialized for each instantiation? I suppose it must if you have struct value types. Do you ever consolidate multiple instances if they're all dealing with dynamic-dispatched reference types? Rust has been struggling to some extent with code size due to multiple instantiation. Especially since we favour vector types: if your innermost function in a deep call tree involves bumping a pointer along a vector by sizeof(T), it causes the whole tree to multiply-instantiate.
Finally: while this is no criticism -- I trust there's a perfectly sensible reason -- I have in the past few days seen a number of articles floating around claiming radically-poor performance on basic arithmetic/array loops, when not compiled with -Ofast (unsafe). I'm curious if you happen to know what the cause of those is. ARC stuff that doesn't get hoisted? Surely this isn't a permanent problem.
(If you prefer emailing privately to posting in a public blog, of course, feel free :)
no subject
Date: 2014-06-11 09:20 pm (UTC)Swift can run generic code generically, using what's essentially a vtable for value semantics. Among other things, this means that we don't have to semantically restrict generics to only places which we can statically specialize: for example, we can dynamically dispatch generic functions and hence use them as class methods, protocol requirements, and even in principle as first-class function values. We're still shaking some bugs out with nested generics, so some of this is restricted, and of course higher-rank polymorphism adds a ton of type-checking complexity that we have chosen not to pursue yet; but the runtime model supports it.
This value-semantics vtable (we call it a "value witness table", and all of our type objects have a pointer to one) contains functions for copying, moving, and destroying values as you might expect; it also contains functions for storing the value in a fixed-size buffer, essentially a void*[3]. Those latter functions are useful for storing values of the type on the stack in generic code, but they're also useful for existentials. Our existential representation is essentially:
In other words, a sort of typically-inline boxing.
As I'm sure you can imagine, this all means that generic code is not exactly fast, so our optimizer has the ability to specialize generic algorithms for specific arguments. In the long term, we think this will give us a lot of flexibility to avoid code bloat around redundant specializations, e.g. by forming partial specializations for types with identical representations, or by choosing not to specialize functions which don't show up in profiles. Right now, the optimizer is just doing aggressive deep clones, so I expect we have the same code size problems that Rust does.
The performance problems mostly all boil down to our optimizer still being very immature, mostly for terrible and kindof embarrassing product-development reasons.
no subject
Date: 2014-06-15 03:45 am (UTC)In fact we treated the "whether to monomorphize" as "just an optimization of vtables" for quite a while, with a variety of attempts at high level partial specialization and reuse; the reuse-management code has all been thrown out because we could never get it correct enough (some type-variability we hadn't counted on always snuck in) and we figured LLVM's mergefunc would (eventually) be able to do a better and more-honest job tidying those cases up for us. Whether so remains to be seen. There are still vestiges of the "static is just a special case of vtable" assumption floating around the compiler, eg. https://github.com/mozilla/rust/blob/master/src/librustc/middle/typeck/mod.rs#L184 and https://github.com/mozilla/rust/blob/master/src/librustc/middle/trans/callee.rs#L228 and such. The term "vtable" is applied generically to "as much method-resolution information as the codegen will need to emit something sensible". I'm sure this is quite mystifying to newcomers on the codebase :(
no subject
Date: 2014-06-17 11:51 pm (UTC)Rust credit given.
Date: 2014-06-03 10:20 pm (UTC)-- http://nondot.org/sabre/ (Chris Lattner)
confirmed
Date: 2014-06-03 10:23 pm (UTC)http://nondot.org/sabre/
(I tried to post this on dreamwidth.org following your request, but it said "Only friends of graydon2 may post in this journal.".)
no subject
Date: 2014-06-26 01:01 am (UTC)