The Curious Case of Swift's Adoption of Smalltalk Keyword Syntax
blog.metaobject.com
blog.metaobject.com
When initial SwiftUI changes came through, they were "proposed" by Apple engineers even though they already shipped in Xcode. Sure, they got a bit of feedback and change, but everyone largely knew they were going in regardless and they did. When callAsFunction came in (which I actually think is a useful feature, just spelled horribly) it was obvious that it was being pushed heavily by the Swift for TensorFlow people. And who reviewed it? Chris Lattner, who approved it even though nobody really liked the name at all. And with this case we see a fairly dubious benefit being pushed forward by the team at Apple, and everyone knows it's going to be for some sort of SwiftUI DSL thing, except they can't really tell us what it is and currently it provides a new the way to call a bunch of functions that is unergonomic and extremely special-purposed.
When people say that Swift is adding "useless features" they don't really think that Swift should stop evolving; it's just that they're frustrated that stupid things like these keep getting added and that the Swift Evolution community as a whole is unable to stop them from being merged in or modified substantially.
This outrage over closures reminds me of tabs vs spaces - it's one level above utter lunacy. Who gives a shit what the syntax is? Didn't we use Objective-C not long ago? Programming languages are a tool for me, not an artistic expression of text on a screen.
What has this language allowed me to build that I couldn't in Objective-C? Let's use empirical evidence - why do we have electron apps for something as basic as a chat app (Slack) instead of a native app?
We're going backwards in software tools as far as I can tell. Everything is more complicated and fragmented and Swift is a part of this mess, not a solution.
Sorry, carry on with expressing opinions about syntax and pointing out that those who pay for the juke box, get to select the songs and are hypocrites when they pretend otherwise and press 'Billie Jean' when most people wanted 'Thriller'.
- direct compilation to native code with static analysis and optimization
- automatic reference counting (more predictable/consistent performance vs. garbage collection)
- good support for native iOS and macOS app development
- ranges
(But on an unrelated note, I have to say it's disappointing to me to find any language in 2020 (Java...) that doesn't support named and default parameters.)
The increasingly complex syntax of Swift IS the problem.
We have furniture and some furniture uses screws to hold it together.
A furniture manufacturer decides to create a new line of furniture and introduces a new type of screw that requires you use a new screwdriver.
This screwdriver and screws are magical, they can magically alter themselves over time in small ways - they can change color, the handle can become rubberized or change texture, etc.
There's a whole group of people working on deciding how the screwdriver and screws magically alter themselves and which rubber material is best, which color is most pleasing, etc. They're so wrapped up in it, they never stop to ask, wait, why the fuck did this new line of furniture even require new screws and screwdrivers? When I compare the old line of furniture and the new, I can't tell the difference! It's the same shitty furniture!
We have multiple furniture manufacturers, why didn't we just agree to use the same screws and screwdrivers for all of them?
I don't get it - the people who are so passionate about screwdriver handles of one single manufacturer, to me, are a bunch of lunatics.
Does that help?
I wanted some replacement shelf supports for a bookcase, and it turned out there are ones that are specified in metric dimensions and ones that are not, and they are like half a mm different, and so the only ones that the local big box store carried were just enough off they didn't work. Even with a hammer. And my drill wouldn't fit in the corner. So I ended up taking the appropriate drill bit and enlarging the holes by twisting it in my fingers, which hurt a bit but shockingly worked as it was a truly tiny bit of additional clearance needed and soft particle board.
It really was annoying in a rather similar manner as programming can be. The fasteners and tools to make cheap furniture should be standardized.
Also Objective-C scares people away. Swift being a nicer Obj-C syntax alone has consequences of it's own. And pretty much nobody in silicon valley / startups / big tech other than microsoft uses C#. C# is anti-recruiting indicator in most engineer's minds.
I also think nullability typing in swift & all major libraries having proper nullability typing brings major benefits, I'm not sure if C# has that.
> Let's use empirical evidence - why do we have electron apps for something as basic as a chat app (Slack) instead of a native app?
I am currently using a native app written in Swift to access Slack, and I am very glad that it exists.
I guess this is generally the reason why some have little faith in Swift and generally prefer Objective-C.
I still remember 15+ years ago everyone thought Objective-C was horrible. And Java, C# syntax was the future.
Somehow once the alternative actually arrived, OC suddenly became not so bad.
I never thought Java would die. Then Oracle saved us all, and bought Sun. And they proceeded to burn down bridges, and destroy all the goodwill that Sun built up with Java.
Now, I can finally see it. The Sun is finally going to set on Java.
The Java is dead! Long live the Java!
> The already weird-ish addition of Smalltalk/Objective-C keywords inside the f(x) syntax: f(arg:x)
I'm a huge fan of named arguments in Swift. It's one of the things I miss most when working with languages which don't have it.
For instance, I do a lot of work with computer graphics, and in that case you end up with a lot of functions with a lot of arguments for things like configuration. With named arguments, code becomes almost self-documenting. Without them, I just end up using comments in their place, but of course then I don't have the compiler making sure this is correct.
It's a totally optional language feature, and adds a lot of value in some cases.
For all the attention given to static typing and unit testing for increasing developer confidence, I think named arguments belong up there as well. Makes reading code so much easier, especially the way they are implemented and encouraged in Swift.
As someone stuck doing PHP for my day job, they could buy an awful lot of goodwill from me by implementing something like this in PHP (well, that, and separating arrays from dictionaries, but that seems far less likely to happen).
As an example imagine a search function that returns the position of a character `c` in a string `str` that looks like that:
int search(String str, char c, bool case_sensitive, bool from_end)
I'd say that I would avoid this signature in general, because those two `bool`s make for an awkward call syntax: search("foo", 'f', true, false)
That's pretty opaque, even if you know what the two options are it's easy to forget the order. Now you might say "well see, that's where keyword parameters shine!" and you'd be right, it does help a lot: search("foo", 'f', case_sensitive: true, from_end: false)
But I'd argue that while this is good enough if you're coding in a dynamically typed language, it's actually sub-obtimal if you're coding in Rust for instance. Here you can do something even better: give these two options different types, this way not only it makes the code more explicit, it also means that it gets enforced by the compiler: enum SearchCase { Sensitive, Insensitive };
enum SearchDirection { FromStart, FromEnd };
search("foo", 'f', SearchCase::Sensitive, SearchDirection::FromStart);
Now that's not to say that I don't want or like keyword arguments, I just think that it's one of these situations where "when you have a hammer, everything looks like a nail". I sometimes miss keyword arguments in Rust, but it's the exception rather than the rule for me.For instance, even in the example you gave, it could be argued that the SearchCase type is really just a boolean which is being introduced to do the job of an argument label. If this enum is only used in this function call, that's going to add noise to the docs with fairly tenuous justification.
And this only works in the case of arguments which can be easily converted to enums. For instance if I have a signature which looks like this:
void search_buffer(int match, int start, int end, int stride)
It's going to be very difficult to look at the call site and infer what the meanings of these arguments are.I don't really agree with this, I think in general it's a good idea to type your parameters as tightly as it's reasonable to do. I don't think that introducing a one-time enum is particularly unreasonable. I don't really get the argument for reducing noise in the docs, typically nowadays API documentations are hyperlinked document, not a big PDF, you either look the symbol up or you don't.
An argument in favour of using types here would be in the situation where the "case_sensitive" parameter is not a literal in the code but something that's being passed as argument from an other function or stored in an object. The names could be slightly different, the conventions could change from one part of the code to an other etc... With dedicated types you can be reasonably certain that if the types match then it's probably safe to pass to the function, with human-level semantics that may require more caution.
Of course my example is so trivial that it could be argued to be over-engineered, but consider a function that takes angles as floating point numbers vs. a dedicated "Angle" type for instance, with the float you have to make sure that the underlying representation is correct (degrees? radians? turns?) whereas a dedicated type would typically have well-defined semantics.
>And this only works in the case of arguments which can be easily converted to enums. For instance if I have a signature which looks like this:
void search_buffer(int match, int start, int end, int stride)
I agree completely with you here. Keyword parameters definitely have their use cases. I just think that they should be used parsimoniously and, IMO, can be a code smell if used too often, especially in languages with a decent type system.Also, if Rust were to support named arguments (even better if with default values), you can use explicit types for your bools all the same. It’s not an either or situation.
If I understand it correctly, the equivalent builder pattern in Rust (which is essentially using methods to build a struct used to instantiate the object) would be:
let search = Search::new("foo","f")
.option_case_sensitive(true)
.option_from_end(false)
.build();
Where "new" returns a builder object/struct with passed in and default params, those options methods are optional and can change those defaults, and the build method actually builders the search object from the search builder struct with all the params.I'm not sure how well this style works with simple functions, unless there's a macro or crate that can do most of this work for you from a a spec.
1: https://doc.rust-lang.org/1.0.0/style/ownership/builders.htm...
Bool is also enforced by the compiler; adding ad-hoc types for things that have only two values is generally not something you should do without good reason. By the way, constructs like these are fairly common in Swift:
enum SearchType {
case literal
case regex
case extendedRegex
}
extension String {
func substring(matching needle: String, interpretedAs type: SearchType = .literal) -> Substring
}
which you'd use as something like "foobar".substring(matching: "o*ba", interpretedAs: .regex) // "ooba"But your point is well taken that, like many tools, you shouldn't abandon one set of good practices for another. There are certainly cases where a builder pattern or enum values are a better fit, which could be obscured as an option because keyword arguments "sorta kinda but don't really" address the same thing.
/// Doc comment doc comment doc comment
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum SearchCase { Sensitive, Insensitive }
now it starts to feel like heavyweight clutter.After all, even with keyword parameters there'll always be plenty of opportunity for one-off enums, if only for situations where more than two choices are possible. Take a typical "seek" function for instance, who can usually be performed relative to the current location, from the start or from the end.
search("foo", CaseSensitive, FromStart)
or options that take values: search("foo", WithCaseSensitivity(false))Maybe it's just me, but I would hate to imagine living in a world where my projects might be incomprehensible if I ever lost my .vimrc
For example if, today, I checked in code like performRequest(request, response, null, null, null) you probably wouldn't consider that good code, as it's not clear what the nulls mean there. Most people today would try to not write code like that. But if everyone was using an editor which displayed the parameter names, then it'd be fine. People would write that code, check it in, and so on. Which would mean that those people without the tool would have to confront such code.
- All arguments are keyword arguments. - Each function implicitly defines a struct type equivalent to its arguments.
I think if you combined this with some nice structural typing you can get a lot of the argument forwarding/builders and similar that people enjoy in dynamic languages without much/any overhead. Rough example:
fn add(a: int, b: int): int { a + b }
fn add3(c: int, rest: add::args): int { add(rest) + 3 }
A somewhat silly example but I think you can see the point. Of course you could also add language-level builder syntax to these structs and have something really nice.
I think the key here is that once you make the arguments a static type you can add loads of convenience like builders very easily.
https://github.com/idanarye/rust-typed-builder/blob/master/R...
The key is that it only generates code for types that actually get used. So if you always call the orders in the same order, it won't generate tons of code. That said, you could actually tweak the type-safe builder to enforce a certain order for calling the methods on the builder.
Also, it's not the order of arguments, it's the order of method calls :)
Do you have any feedback on that article's thesis that the closure syntax could have been handled better?
Every programming language changes as it evolves and matures and people understand how to use it best or its domain changes significantly. Unfortunately, this always means accumulating these syntactic and semantical warts. Personally, I think Swift manages to keep a good balance, and it's a very powerful language, with lots of users and compatibility requirements, so it's much harder for them than for many others.
Alternative is constant redesign of syntax and behavior that just keeps making users unhappy with every release, breaking their code, libraries and obsoleting the docs.
If, to mitigate that, you try to make big changes to the language in big releases, once in a long while, you can easily get into the Python 3, Scala 3, Perl 6 fiasco and that can simply kill the language adoption.
It's a hard problem.
As regards Perl 6, a recent development was to rename it to Raku, which (in hindsight) seems like a perfectly sensible decision. The overloading of the name Perl for a language that proved to be something different from Perl 5 caused trouble both ways. Perl 5 was widely considered to be a version that was to be eventually be replaced, whereas Perl 6 was considered a language that was a close and direct iteration of Perl 5. So we basically got the worst of both worlds. Perl 5 is very much in use today, still being actively maintained and is getting new features regularly, while Raku/Perl 6 is a beautiful new different language with interesting ideas being worked on by many brilliant folks, and the notion of trying to be in any way compatible with Perl 5 seems utterly irrelevant today.
I know they aren't big because Perl has had to deal with many of those same type of problems, only it didn't have to break old code to do it.
Perl 6 is more of a research project that took Perl 5 as a leap-off point. As it is is such a large departure from every language, it deserves it's own name. So it is now known as Raku.
I'm a bit perplexed by the author's position that Swift should just be small-talk as though this is obviously better.
As far as the idea of getting rid of parens entirely as the author suggests, I'm not a big fan of this approach. In languages like ruby where parens are not used, I tend to find this makes things a lot less readable. That might be a matter of opinion, but at the very least I think it can be said that Swift's approach to arguments, where only closures can be moved out of the parentheses of a function call is a perfectly consistent and defensible approach. For instance I don't think that this:
foo(someArg: 42) {
// do something
}
Is less clear than this: foo doSomething: {
do something
}
someArg: 42
In fact I think it's probably the opposite, and the only reason I can imagine to prefer the second example would be out of some appeal to purity/simplicity of the function call syntax over any level of pragmatism.As far as the whole debate over the inconsistency of the first argument label being removed while the following argument labels are required in multiple-trailing-closure syntax, I just don't think it's that big a deal. I also understand the consistency argument, that it should be handled the same for all arguments (i.e. the author of the function signature should explicitly decide if the label is required or not), and it probably should work that way, but I also understand why it hasn't been proposed that way for the first iteration. It would break a lot of code given how prevalent trailing closures are in Swift.
However there are already proposals in the pipeline to introduce an argument label in the first trailing closure (provided the function author hasn't named it `_`). I suspect in the long term this will eventually make it into the language, and automatic migration tools will make it possible to resolve issues with existing code without much fuss.
So in short I think multiple trailing closures are a good and natural addition to the language which provide real value, and the proposed syntax is absolutely fine.
Passing around objects is precisely the workaround people use for lack of named arguments.
In my opinion, languages should be simple and small, relating on composition of core syntax features over introducing new syntax for each and every new stylistic fad happens to be popular this year.
It cannot. Because, as you yourself said it: the function must know how to deal with object-as-parameters.
> relating on composition of core syntax features over introducing new syntax
This doesn't work for named arguments. They either exist in a language, and you can use them everywhere, or they don't, and you're left with inconsistently implemented workarounds.
And what I’m saying is that if they don’t exist in a language and you can instead using other more core things in the language to implement them rather than adding new syntax (complexity) to the language itself, that’s (in my opinion) a better language and the one I’d rather work in.
Once again: these are workarounds and there are no "functions declared with named parameters" in Javascript. There are functions that accept objects as a parameter, and they employ additional code to work with those objects.
Example:
function add(x, y) {}
No matter how much you try, you cannot "use the core thing in the language" to make this function accept this add(x = 1, y = 2)
// or
add({x: 1, y: 2})
without writing additional code inside the function itself.So if a library gives you `function add(x, y)`, that's what you're stuck with, forever. You cannot pretend that named arguments can be solved by "javascript's core language". It cannot.
Yes, there’s no dedicated syntax to allow people to name parameters that weren’t intended to be named. So what?
Of course it's not complicated, it's as simple as I showed.
> If I’m making a function that has potentially confusing parameters, I make it accept an object.
It doesn't really matter what you, a single developer, do, does it?
If I have to work with a third-party library that has "confusing parameters", please enlighten me how I can make its functions "accept an object to force named parameters"?
> there’s no dedicated syntax to allow people to name parameters that weren’t intended to be named. So what?
I've clearly shown what in at least three messages.
Keyword parameters are great. The way they are done in x.f() syntax languages is sub-optimal, and Swift is particularly weird.
> code becomes almost self-documenting
Yes, exactly!
Dan Ingalls explains this wonderfully in his Object Oriented Programming talk. Highly recommended:
https://youtu.be/P2mh92d-T3Y?t=515
The problem is that there is the Smalltalk/Objective-C style keyword syntax or the Pascal/C/C++ etc. function syntax. Both are consistent and complete ways of handling this, though I obviously think the Smalltalk way is better.
But it's either/or. When you jam them together, you get a weird mess of special cases and workarounds.
I would agree that the benefits of labels outweigh the messiness, even for the x.f() + labels case. So if you're coming from the x.f() side of things, I can fully understand and sympathize the view that they are a pure benefit without drawbacks. But once you have keyword messages, you no longer need the x.f() part of the syntax, because the syntax is completely self-sufficient. And then all the extra messiness of integrating the two makes no sense whatsoever.
As an example, the colon and the whitespace are sufficient to separate parameters. So is the comma. So we have...both?
There is also the issue that, please correct me if I got this wrong, the label that's in the signature becomes the name of the parameter inside the function. And as far as I can tell it cannot be changed/mapped. That's a horrible coupling of inside and outside.
Because of this, you actually need labels in your function declaration even for parameters that are unlabeled. Wut? And because of this, you need another special bit of syntax that says "despite the fact that I am now about to give a label, this is unlabeled". It also means that a name that is actually purely internal to the function is part of the signature. Wat?
And sometimes you get partial labels that just kind of sit there. It's really weird when you look at things closely.
I guess an analogy would be like a gasoline engine, a hybrid and a fully electric. Yes, the hybrid seems like an obvious improvement to the pure gasoline engine, if at some cost in complexity. But the pure electric car (let's assume a few more years improvement in battery tech) does away with all that extra complexity.
Hmm not entirely sure that analogy is all that good.
The way Swift does them is the least awkward and most robust I have seen in any language.
> As an example, the colon and the whitespace are sufficient to separate parameters. So is the comma. So we have...both?
Designing a language around what the minimum requirements of a parser is will not get you a good language. Human language is massively redundant, because humans find it more easy to communicate when there is redundancy. Computer code is also communication aimed at humans, and a bit of redundancy does not hurt there, and can often help.
> There is also the issue that, please correct me if I got this wrong, the label that's in the signature becomes the name of the parameter inside the function.
This is incorrect. You can optionally specify both an internal and external name for any argument. Unlabelled arguments are just a special case of this syntax, where you say "external name does not exist, internal name is this".
You got that wrong. You can specify a separate internal name like this:
foo(externalName internalName: Type) { ... }
> you actually need labels in your function declaration even for parameters that are unlabeled. Wut? And because of this, you need another special bit of syntax that says "despite the fact that I am now about to give a label, this is unlabeled"I mean you need to refer to those values inside your function as something and that has to be specified somewhere. How else would you do it?
Also the `_` you're complaining about is just a special case of the syntax I described above. This just means the external parameter name is omitted at the call site. It's quite consistent: if two names are given, one is external and one is internal. If only one is given, that means the external and internal names are the same (which is convenient since this is the common case).
> It also means that a name that is actually purely internal to the function is part of the signature. Wat?
Again, where on earth else would you put it? That's only also half true: the public signature does not include the internal parameter name. For instance I can declare a protocol like this:
protocol P {
func foo(arg: Int)
}
And both of these implementations will conform to it just fine despite the fact that the internal argument names are different: struct A: P {
func foo(arg internalA: Int) { ... }
}
struct B: P {
func foo(arg internalB: Int) { ... }
}
> And sometimes you get partial labels that just kind of sit there.Can you give an example? I'm not quite sure what you're talking about.
Ahh, my bad! But only the "cannot be changed/mapped." part, right?
foo(externalName internalName: Type) { ... }
Doesn't this strike you as odd at all? The part that looks like the label is not the label, it's the name of the variable. And then there's this extra bit in front that doesn't look like the label, doesn't really look like anything TBH, that will turn into the label. If present.I kind of see how they got there, but boy is it inconsistent.
It seems pretty simple to me: everything before the colon is defining the argument name. If you have one label, it means the argument label is the same as the name of the parameter used inside the function. If you have two labels, then the first one is the argument label and the second one is the variable name.
You could also think of it that you’re always specifying the argument label and the variable name, and in the case where they’re identical you have a bit of shorthand to save some typing.
Maybe it seems weird to some people if it’s not familiar, but I got used to it pretty quickly after starting to use Swift, and it felt like a natural extension of the type system and optional handling in terms of the language giving me tools to cross-check things and bake sanity checks into the source code itself.
Probably. ¯\_(ツ)_/¯
> everything before the colon is defining the argument name.
So the space binds more strongly than the colon. That seems..."unusual".
You can in fact provide names for both, and there at least used to be (I think they might have removed this?) some ludicrous special case on the behavior of the first parameter.
I think named arguments is an example of trying to solve issues at the language level, when the real problem is the information that is hidden from you while programming.
Think: f(fst:x, snd:y), what is the type of f(snd:4)?
I think this can be implemented, but then f(3, fst:5) is either an error or counter-intuitive.
In practice the order and identity of the arguments is quite clear in Swift.
Is there a good intro to Swift that goes through only what is unique/peculiar about Swift and explains that in a good way to someone that already knows how to program?
# let f ~fst ~snd = fst + snd;;
val f : fst:int -> snd:int -> int = <fun>
# f ~snd:4;;
- : fst:int -> int = <fun>Without overloading you could define the answer as
g(x) { f(fst:x, snd:y) }
always which would make partial application somewhat ergonomic but makes allowing fst without snd horribly ugly when you have to pass in some spiritual null or _ to.I think these ideas are fundamentally opposed and so I don’t think there’s a good way to have a nice syntax for both.
f(fst = _, snd = y)The one largest obstacle for partial application (at least of the Haskell style) is optional arguments. You just can not have both.
But yeah, functors are all about building stuff like that. So there are similar things, just with a different syntax.
Now there are so many ways to the same thing it must be a nightmare to teach. Each way of doing the same thing is in its own way nice, but they seem to just be accepting every half-way decent proposal for things they've already solved.
I feel like SwiftUI had brought a number of challenges that have massively complicated the language and you can really feel it when you bump up against things that have been tacked on to make SwiftUI work (which it looks like they are getting around to smoothing out now - multiple trailing closures is very a much a problem compounded by SwiftUI).
It's a shame, I loved this language from v2 -> v4, v5 was a good release but started to muddy the waters and now its just getting continually worse.
I've been a huge fan of Swift for years, and it's still one of the languages I reach for most for personal projects, but I was really put off by the way the rollout was handled for the features supporting SwiftUI, specifically property wrappers and function builders. Property wrappers in particular feel like a half-baked solution to paper over the lack of real meta-programming facilities.
I think it was especially disheartening the way these changes were sprung on the community after being shown at WWDC. It just made the promise of Swift being an open source project built through a community-based process seem like a bit of a farce, and made me a bit more apprehensive about investing in a language which might radically change in direction based on the product needs of Apple.
Swift idiosyncrasies don't even put a dent in the learning curve imo.
I don't think anything gets this across quite like onboarding experienced devs into a client app that haven't done mobile client dev before. They'll ask you completely reasonable questions about Appkit/UIKit/CoreData like why is NSBox used here and you realize 90% of the time your best answer is just "well, that's just kind of how you learn to do it over time." Very few questions about Swift itself.
apple allowed this problem to happen. it's the same as allowing people to abuse SEO to get on the first page of results. apple told people that there is money in iOS and they went there.
we are the worse.
nobody is teaching you how to write a macOS app. heck, metal has been out for 6 years and there is zero book that will tell you how to set it up on the mac. if that goes on, metal will die because only few will use it. people are like metal is for library and framework and game engine builders. don't use it directly. yeah, and it is competing with and replacing opengl or vulkan. and people are using those directly. and there are bazillions of resources on those. kids are writing shaders in unity.
oh let me give some kudos to the apple programming guides because they deserve it!
i stopped looking for books and watching videos because of the iOS thing mentioned above. everything i needed was on developer.apple.com and i didn't know it. i started with the metal programming guide on my phone a few weeks ago. but that guide made it clear that i needed to understand the ecosystem first. so my detour took me through many others: the cocoa, the event system, the view hierarchy, etc.
these guides have been the highlight of the past few weeks for me.
they are better than most programming documentation out there. and i read them on my phone! i have 3 of them loaded in chrome on my phone right now. i still don't know what makes them so great but i intend to write about that at some point. one thing i figured is that they are light on code sample. reading code on the phone is really the worst.
sorry long comment, but yeah, swift is not the best thing. i am finding great joy in objective-c and what came before because i went through these guides.
Swift appears to be a language for the captive audience of iOS devs, who are able to keep up if that's all they're doing. I don't really see it being adopted anywhere else.
I think it's actually fair for a new language to take a somewhat cavalier approach to backward compatibility. I would rather not be stuck with all the naive mistakes made in the first version of a language just to avoid migration.
Swift 3 was released in 2016. Swift is ABI / source / module stable now, but that doesn't make it retroactively stable with a version released four years ago.
Of course this comes with its own concerns, as you can see from that thread.
I think you may be referring to the promises of ABI stability? They kept promising that "soon" starting around Swift 3 (IIRC) and kicking the can down the road for a while there, but it is stable now on Apple's OSes. But I don't recall seeing promises of source stability back then; they were pretty open about the fact that the language was still evolving.
But the Swift team / Apple doesn't seems to be aware of it. ( At least a few internal Apple devs completely reject Swift for their project.) That is why I think Chris Lattner trying to make Swift scale from Javascript down to C was wrong from the start. And Swift will be like C# on Windows.
That could be it, or that could be that these days any language that doesn't have a syntax vaguely resembling C on one hand or Python on the other it is considered niche or "weird". You'll also have an endless stream of people commenting (mostly from the Python side) that your language is noisy, because they won't take the time to understand its syntax and it won't read like pseudocode, and that's a bad thing for some reason. But I digress, because that's a pet peeve of mine.
Anyway, my point is that if Swift had Smalltalk-like syntax it would not have been nearly quite as marketable. The situation with Javascript is somewhat similar: under the hood the language is more like a Lisp than C, yet it awkwardly uses C-style syntax (and weird rules for automatic semicolon insertion, because that's more like peusdocode that way, and that's a good thing for some reason).
This was one bit I left out: with Objective-C, Apple had firmly established keyword syntax in a Tiobe top 3 language, albeit within square brackets, so there was absolutely no need to do this for "marketing". It truly was snatching defeat from the jaws of victory.
> that your language is noisy
Smalltalk is about as syntactically quiet as you can get.
Objective-C ranked highly in usage, but largely because of it being necessary for iOS development.
At the same time, it was common outside of the Apple bubble for devs wanting to make something for iOS to complain about having to learn this "weird language with square brackets" that "only Apple uses", and "why can't we just use something like Java like we can with Android?"
With, of course, zero knowledge of Objective-C's Smalltalk roots, or its purported ethos.
I know it's more complicated than "square brackets bad", with all the C code mixed in, and memory release pools and all, but in general, Apple did still have a marketing problem courting outside developers to build native apps without resorting to "web view" wrappers.
I love Swift, I really do, but I would love something that feels more directly Smalltalk-esque even more. Swift decidedly feels like the goal was to make something that looked familiar enough to the C / Java / Javascript / C-like devs to get them on board, without tossing out some of the core benefits of the Smalltalk ethos. Which, apparently, means using C-like methods with parentheses at first... until you reach special cases where you don't.
Smalltalk is, IMO, a great example of how something can both be very expressive yet very minimal... but that inherently means ditching some very familiar syntax. As soon as you do that, you lose a lot of C-like developers (be it rightfully or wrongfully so).
Apple categorically did not have a problem courting outside developers at this point in time. They hit 20m registered developers in 2018:
https://techcrunch.com/2018/06/04/app-store-hits-20m-registe...
Yes, this was after the Swift announcement, but the trajectory was in place way before that.
WWDC has been completely sold out for how many years? At first, it sold out in a couple of weeks, then a couple of days, then a couple of hours and finally a couple of minutes or even seconds. Then they started doing the lottery and even that was a while ago.
> but that inherently means ditching some very familiar syntax
Swift has tons of unfamiliar syntax. And you don't have to adopt Smalltalk as-is. Objective-Smalltalk, for example, has class definition syntax and uses curly braces. Those are not big deals. Keyword syntax, on the other hand, is a big deal, as the example of Swift shows.
Because you do want argument labels, and you do want to pass closures. But that just gets messier and messier.
I think the problem is not whether the language is really noisy or not, it's more whether the language is familiar or not. Nobody complains that "(x + 1) / (y * 4)" is noisy, even though it's more symbols than numbers or letters. Everybody is familiar with mathematical notation.
I think deep down a lot of the "noisy" complaints are really about unfamiliarity, and that was my point: most coders aren't familiar with Smalltalk but most coders are familiar with C-style syntax, so it wins by default in the marketability department.
It's a bit like when people who are first exposed to Lisp say "oh my god all these parens!" or when Python-first coders have to code in C and say "wow, I can't believe I have to put semicolons everywhere". Yet when one is familiar with these languages the parens/semicolon kind of fade away because you see the structure behind the tokens, so to speak. I don't even consciously notice semicolons anymore, and I type them without thinking as naturally as I hit "return" at the end of a line. But if you're just starting it does take a while to get used to them.
There is a workable style for C and C++ (and other "C-likes") which looks like this:
int main(void)
{ int ch;
while ((ch = getchar() != EOF)
{ putchar(ch); }
if (ferror(stdin))
{ return EXIT_FAILURE; }
else
{ return 0; } }If you're working in an early-return-hostile codebase it gets difficult, though.
Comparing Swift with other important languages I have used such as C and SML, I think what's missing in Swift is a philosophy that inspires, motivates, and guides its development. I think Chris Lattner described Swift as a modern, practical language. Those are general characteristics but not the basis for a real philosophy.
Others have described Swift as a "car crash" or "kitchen sink" language and I think those metaphors are apt. Swift seems to be trying to be imperative, functional, OO, and reactive all at the same time, with Obj-C compatibility thrown in for good measure. Complexity and intractability will inevitably follow.
The fact that you have a kitchen sink of language features lets you carve out the language of your choice and write code according to whichever paradigm suits you as a developer.
In spite of that, it might be a matter of opinion but I generally find Swift code among the most readable given the nature of the type system, and decisions around named arguments and when and how type inference may be applied.
> The fact that you have a kitchen sink of language features lets you carve out the language of your choice and write code according to whichever paradigm suits you as a developer.
With regard to 3rd party code, you're also free to chose libraries where you agree with the interface paradigm they've chosen.
I find this concern to be a bit overblown. In practice I've never really run into a case where somebody is doing something really exotic and inscrutable in Swift, and it's generally easier to understand than, for instance, a really esoteric Scala code base or a Rust project making heavy use of metaprogramming.
- Teams: Less experienced developers will use fancy features trying to be smart. Every subset of the language finds it way on Pull Requests I review.
- People change teams
- People change companies
- Third party code: It's not realistic to filter out libraries based on their use of the language. Who even has time to review that? Not to mention that high quality libraries are scarce enough that if you start to filtering out libraries you often end up with none.
And the code doesn't have to be "exotic or inscrutable" to be a negative. It just needs to be unnecessarily of high cognitive load for it to impact use of the language. I've seen Swift developers squint at Pull Requests trying to gasp what certain part of the code was doing. It ended up being needless sugar and in turn a bit of everyone's time was wasted.
That mindset of "just pick whatever part of the language suits you" is why C++ is what it is.
You don't need `guard` if your compiler disambiguates nils and assigns vars in if statements.
You don't need hundreds of hardcoded If<T1>, If<T1, T2> etc. types to support SwiftUI if your type system is actually designed to handle this. And you have proper expressions. And proper functions.
You don't need an awkward `some` keyword if your type system is designed properly.
You don't need callAsFunction because that's the compiler's job, not the programmer's. And can be replaced with any number of things, including (but not limited to) currying.
And the list just goes on.
guard solves exactly one problem, and that's the one where if you have an if-let it makes your code indented. The compiler has support for the things you mentioned.
> You don't need an awkward `some` keyword if your type system is designed properly.
Swift's type system did handle it "properly", the issue was that with the type system you'd have this horrible monstrosity of a nested type when you really only cared about some very minimal erased part of it.
> You don't need callAsFunction because that's the compiler's job, not the programmer's. And can be replaced with any number of things, including (but not limited to) currying.
callAsFunction solves the problem of a closure having associated state that you can query, making them more object-like.
> Swift's type system did handle it "properly", the issue was
> callAsFunction solves the problem of a
From the article :
Swift is a crescendo of special cases stopping just short of the general; the result is complexity in the semantics, complexity in the behaviour (i.e. bugs), and complexity in use (i.e. workarounds).
When I read complaints like this I wonder if the author has spent almost no effort trying to learn the language and just wants to be able to sit down and write Go or C#.
I tried to write a small project in Swift and I really have no intention on ever going back to Swift. It's not a designed language. It literally is a kitchen sink of quite poorly thought out features and "we couldn't be bothered to design a solution, let's just throw in this patch".
The impression I get of Swift is that it takes a bit more effort to get over the learning curve since it introduces some novel concepts (null handling, affine types, the protocol system) but mostly it makes a lot of sense and becomes very nice to work with after getting over the hump.
C# seems easy to pick up by comparison because it's basically like a Java dialect and you just need to map the keywords etc.
https://news.ycombinator.com/item?id=23520626
> since it introduces some novel concepts
They are not terribly novel
Of the features you mentioned:
- guard is consistent with the priority Swift puts on explicit null handling. An optional is never transformed implicitly into a value. I actually think this is a very good thing and makes Swift easy to reason about in practice, even if it occasionally adds a bit of boilerplate.
- variadic generics are a hole in the language, I agree with you completely there.
- as far as the `some` keyword, existential types are also a concept in Rust and for good reason. With a powerful type system like these languages have, it's a distinction which has to be made in some cases. How would a better type system solve this problem?
- How would the compiler replace `callAsFunction`? Also the inclusion of callable types is consistent with the design philosophy of Swift to allow the user to express interfaces in which ever way he sees fit.
Honestly your criticisms read like you're trying to find fault with the language without making the minimum effort to understand why these features might have been designed the way they are.
I have to do front-end iOS work from time to time and it's surprising how bad auto-complete can be in Xcode with Swift even after all these years: still randomly sluggish, randomly stops working inside closures including completely losing syntax highlighting inside the closure, and even Cmd+clicking to see the definition is a hit-or-miss as it sometimes opens up a blank editor and just spins. I would have thought that Swift having a strict type system would have made the IDE experience better, not worse. Don't know if the grammar is too complex or if it is the bridging with ObjC stuff or something else that made it difficult to build a robust solution. Can anyone shed light on this?
My points of reference are rust-analyzer, or just VSCode with Typescript, both very snappy although of course the languages are very different. If anything I'd expect Swift to be closer to Rust experience-wise since they both have pretty strict grammars.
killall SourceKitService- Limit interaction between Swift and Obj-C wherever practically possible. Swift headers for Obj-C functions are autogenerated by the compiler, which can act as a bottleneck depending on how much Obj-C is in your project, how often that Obj-C changes, and how often you clean the project.
- Avoid nesting closures deeper than 2 levels (probably good advice anyway).
- Explicitly state types where possible to avoid inference.
- Avoid excessively long chains of optionals and/or casts.
So generally, try to keep code clean and explicit, and when choosing dependencies opt for pure Swift options where viable. Avoid adding new Obj-C code except where needed (e.g. C++ interop) and if possible reduce Obj-C in the project to small, seldom changing bits.
Well with one caveat. It only works well if the file that I'm currently editing is in the selected target (i.e. if I have a file Bottombar.swift and that is in InterfaceKit.framework, and I'm editing it while the selected target is ExtensionKit.framework, then nothing works)
A trillion-dollar company cannot create an IDE on par with JetBrains which has 1200 employees, 250 M revenue, and and has not one, but several IDEs and plugins for several languages (including dynamic languages, including support for multiple versions of a language, including multiple versions of runtimes for a language) that are superior to Apple's one single IDE for two languages.
It's not a "millions into Java" problem. It's an Apple problem.
Now you’ll have to read and write yet another syntax variant, with slightly scrambled separators and brackets.
No language is perfect, and it's important to be cognizant of the tradeoffs and flaws. There's no denying that Swift is a powerful language in many ways. But there's also no denying that the chosen means to that power has costs. The most obvious is a lot of complexity, both essential and non-essential. Which brings us back to the article and the peculiar way multiple trailing closures ended up being integrated.
Though having used Python a lot, I've also become something of a fan of braceless, indented syntax, which I find to be clear and conserving of vertical space.
But I do understand the main argument for making it C-like: they wanted to make an easier transition for Apple developers who were already using Objective-C, and others who might be coming from Java/C#/C++/JavaScript/et al.. Also Smalltalk's syntax, while simple, orthogonal and versatile, simply never caught on the way C's did.
So I left it alone ASAP.
Now, my fears are confirmed. Swift is now iPerl.
element.animate {
duration: 300ms,
completion: () => this.remove()
}
Explanation, this syntax construct nmtoken {...}
is just a sugar for this: nmtoken ({...})
And that is good old function call with single object/map literal as a sole argument.By the way this construct can be added to JavaScript without breaking existing code.
It reminds me, I believe Lua has similar syntax sugar that works with tables as well as strings.
fn "hello"
is equivalent to fn("hello")But I'm not above playing with sugar, including Smalltalk-inspired ones. In Racket, as an exercise, I implemented a straightforward way to do Smalltalk-like syntax for procedure calls.
If the head of a list expression is a symbol that ends in a colon, then it was assumed to be the Smalltalk-ish procedure name sugar. A syntax like this:
(foo: bar from: (+ 1 2) to: (funky 3))
would be translated by the reader to this procedure application: (foo:from:to: bar (+ 1 2) (funky 3))
You'd probably define the procedure in the usual Scheme way, like this: (define (foo:from:to: source start end)
...)
Of course, I wouldn't use this complicating sugar in real code without a good reason.