I’m new to Swift. It’s exciting that it’s an evolving language and I’ve supported some recent proposals but this looks like a new thing to learn for no clear benefit. To encourage developers to give Swift a try language changes need to be more meaningful.
I disagree about the remark on multiple trailing closures. In general, passing closures to functions results in redundancy in the syntax, because commas and parentheses don't add information when you have already the brackets that clearly delimit the scope of a closure. The discussion was mostly about how to "group" trailing closures and handle their parameter labels. Again, I don't want to start the same discussion all over again, but the vast majority of users/designers of higher-order functions (myself included) really appreciated the improvement in the syntax, even with the controversy related to the first parameter label: this is a problem in many other languages, and the accepted implementation made Swift better than them in this regard. Many syntactic features of Swift make the language better than many other languages that kind of "compete" with Swift in the same space. The trap in which we should not fall, in my opinion, is the "scripting sugar envy" one, where sugar that exist in other languages for scripting purposes is considered as an addition to Swift, a language that, as being already said, has different goals when it comes to "expressiveness".
If anything, the multiple trailing closures proposal pushed the syntax forward in a language-defining way that further differentiated Swift from the mass of "general purpose" languages out there, and further contributed in defining what's "Swifty", and the result is, to me, really good.
Omitting the return type of a function when the same type can't be clearly inferred by the context just by reading the code, and not with IDE support (and this is one the main pain points to me when I work on Android with Kotlin) is something that I would not allow in production code. Replacing brackets with something else to delimit a function body is not something I would oppose to, in principle, but there must be a very, very strong case for it (and "this other language does it" is a weak argument), because brackets are, to me, an almost perfect, not to mention familiar, way of doing the same thing.
I'm not buying this 'redundancy' in syntax argument is bad. There is a lot of redundancy already in Swift. A lot of these syntactic sugar additions (1) provide no new functionality (2) require the parser to do extra work to make exceptions. Regardless, I am more concerned about clarity and consistency. It is not consistent to have multiple ways of doing something. There is nothing arduous or clunky about putting all arguments of a function inside the () in such a way that matches what the declaration looks like. Not having the () requires the parser to look for exceptions. It also doesn't make the language more powerful because it adds no meaningful power to the language. Ones code isn't magically doing new things. It does however make the language more inconsistent which is not an improvement. There is nothing arduous about typing a ')' at the end of a function argument that happens to contain a closure. I can concede and say that if there is only ONE closure and no other arguments, then YES, the trailing closure syntax is fine even if to me it looks ugly and jarring.
There is nothing arduous about typing {} either which yields the '-> instead to introduce single line getters' moot. Having -> doesn't provide any new functionality. BUT it really is exactly the same argument as trailing closures, multiple trailing closures or any of the new proposals to strip away syntax will make.
There are lots of things we could remove in the language to make it more (non) Swifty. We won't need array literals when function builders allow the creation of infinite number of objects within what used to be a subroutine body. We will the argue that we should remove ['] form the language, because one could easily just have a function builder make the array. Then we won't need any commas anymore! The complier/parser could easily be reworked to use {} instead of '[', because just like in trailing closures, you can build the parser to look at the contents and realize what it is and just transform it to an array. I won't be surprised when I see this exact proposal. (This will never happen for so many reasons mostly to do with parsing, and I don't want it to, but the argument will be made)
We also don't need 'var' anymore. Function parameters don't need a var keyword. Why do we need to type var. It's redundant. The parser should already know that the first token on each line within a structure's body that is followed by a : is a variable. Having to type 'var' is not Swifty.
We also don't really need to func keyword. The parser should know that a token that is followed by a ( is obviously a function especially if it is then followed by a -> a type and {}. It's redundant to have to write func. Removing 'func' would make Swift more readable and will be a welcomed addition.
By removing var, and removing the need for arrays to be created using '[' we will push the language forward. It will also align variable declarations with argument declarations -- so the language will now be consistent.
We can make the parser do many tricks.
When we @ autoclosure to elide braces in argument lists, the user doesn't necessarily know that the argument will be treated as a closure. The signal is only visible when inspecting the declaration (which you may not notice ever if you didn't writ the declaration yourself . I think others would use that as an argument that there is precedence for eliding the {} as well.
I would then argue that @ autoclosures were an oversight and should also be removed.
I think you are:
- making very broad and unhelpful generalizations;
- conflating what the compiler does when parsing code with what the human does when reading it.
I'll try to answer to most of the points.
From the human standpoint, redundancy is generally bad, and the fact that there's a lot of redundancy in Swift doesn't put the language in a good position, hence proposals to remove it are more than welcome.
True in general, but there might be valuable exceptions: a single exception in a very specific case doesn't qualify for "multiple ways of doing something". An exception, in Swift, is for single-expression closures, computed properties and function bodies: they don't require return
, and this makes the language better, and more readable, as was very very clearly stated in the proposal and argued in the discussion at the time.
If it's better for a human reader, and not particularly hard for the parser, I certainly don't have a problem with that. The capabilities of the parser are hard constraints, not arguments in favor or against something.
This is not the purpose of syntactic sugar.
It's very clearly not the same argument, because is not an "argument": is a proposal, an option, a possibility. The argument would be "it makes the code more readable for a human", and in this case, in my opinion, it does not.
I'm just quoting these from the list of hypotheticals that you made, that are all missing the point in the same way. var
, for a human, means that the thing is variable, and not constant (let
), so it's not redundant: it's very important piece of information. And for functions, even if the parser could deduce it easily (but you are probably greatly oversimplifying the matter) a human wouldn't: a human would like to read that that's a function, and the keyword function
would spell it in the most explicit way; Swift uses func
, which is clear enough, and less verbose, so it's a good tradeoff between readability and noise.
This is irrelevant from a code readability standpoint.
The purpose of @autoclosure
is not to elide brackets, but to make a parameter evaluated lazily, which is not something that the user of the API should care about. A "manual" way of doing this, of course, is to use a closure that takes Void
as input, and the way Swift does it (for consistency) is to automatically create a closure around the expression or value passed to the parameter. This could be done in a different way, but @autoclosure
seems consistent with how the language works.
I think that this proposal and its discussion really bring to light the need to have a harmonizing philosophy in Swift and well defined terms when describing goals. Also, I want to thoroughly thank you for this discussion.
There is this word that keeps being tossed around here, 'readable'. I have a problem with that word because we have no defined system to evaluate if something is readable. I don't personally think that trailing closures are readable. I think they are messy and a horrible deviation from the syntax described at the definition site (the patterns do not match). I don't think that autoclosures are readable because they actually function very differently and have far-reaching implications; you have to read the definition site to know it is an autoclosure (in the opposite way that including 'override' is enforced). If the call evaluates later, you might get a completely different result -- this isn't safe from a Swift standpoint of being explicit. The cue '{}' is elided so you might not remember later that it is in-fact a closure that could be evaluated later on and the value then will be different. I used to document call sites with notes that a particular parameter was an auto closure. I gave up and just started putting in the {}. Luckily, Apple doesn't suggest we overuse autoclosures (I wonder why). I don't think that eliding return keyword is readable (though I am less concerned) -- again a cue is being removed. I don't think that removing 'var' statements would be readable either (but I am sure the proposal is coming). I don't think that -> as syntactic sugar for a getter is readable because it hides what is really happening and only has a very very specific use case. Each of these items remove cues from the language and require the reader to understand two divergent ways of expressing the same thing.
I am willing to accept, however, that you and I are just coming at this from a different perspective. And that each of our positions are just esoteric to the other. At least we both agree that the use of -> in this proposal should not move forward.
HOWEVER, because I am very concerned that I don't understand your perspective. I want clarification on what 'readable' means to you. I am certain I can be persuaded.
What a 'human' might want to do is almost a moot point, because a human would want to do what they are used to. There are languages that don't require a 'func' or 'function' keyword. You do not use the keyword in JavaScript Classes at all (which confused me at first), you simply write identifier() {}. Swift could easily adopt this convention. They are both 'readable' to me. I know exactly what is going on. But I'm a software engineer! As a human I don't want to have to write 'override' every time I override a function. It is redundant, but we put it there because it is a cue that something important is going on and to keep us reminded. It is more 'readable'. Right? But autoclosures that remove a cue are also more readable?
I don't know how to evaluate the concept of 'readable' in terms of Swift anymore. There used to be a standard that Swift should be explicit (through cues) and that is how we achieved readability. So how do we develop a better idea of 'readable'? Or is 'readability' less important and are we just arguing for what we like to be included?
I understand this concern but, as a general point of discussion, it's off-topic here, so I'd suggest to create a specific thread in the Using Swift section of this forum.
In relation to many of the things you're talking about (eliding return
, multiple trailing closures, et cetera), pitches, proposals and discussions were made that also tackled the "readability" point, so I'd suggest to go back to those threads and read them: it's not useful nor productive to bring back, again and again, points that were thoroughly discussed, with statements like "we should never have done this".
I'll try and answer to some of your points, to not give the impression that I'm ignoring valid concerns, but please consider that it's not my intention to discuss these things here at large, both because many of those points were already discussed elsewhere, and because I'd like to remain on-topic.
"Readability" means exactly what it seems: the fact that the code, or more precisely, the semantics of the code statements, is understandable by just reading it, as opposed to needing to read documentation for each call, or to inspect the implementation of each function. Books are written on this topic (Clean Code comes to mind, for example).
Of course the more you're familiar with some syntax, the more you'll understand it, but this doesn't mean that any two languages perform the same way on the readability scale. One example is, how easy it is for a person that's not familiar with some language, but has a basic background in programming (for example, has some idea of what "control flow" means), to start learning that language. A language with a lot of symbols, uncommon operators, and abbreviated declarations, is probably going to be harder to learn, at least initially, than a language that spells out things in english, even at the risk of being verbose.
Swift takes a stance here: its constructs tend to look like natural language, and follow a "progressive disclosure" approach, where you can start with a simple subset of operations, as verbose as needed, and gradually adopt more concise and sophisticated constructs.
Assume there's a protocol Callable
, with an associated type Input
. To extend every type that conforms to Callable
if Input
is Int
, in Swift you write:
protocol Callable {
associatedtype Input
}
extension Callable where Input == Int {
func foo() { ... }
}
This is very close to how you would spell this in english, in natural language. Consider how you would write this in Kotlin:
interface Callable<Input> {}
fun Callable<Int>.foo() { ... }
This is clear to those who know the language, but it's not really "spelled out" in any way: I'm defining a foo
function (the fun
keyword seems pretty clear) but it seems defined in the global scope; I can see the Callable<Int>.
prefix, which it's supposed to provide some cue, but I'm not sure about it... it looks like a call of a static function (directly on the type), but I'm defining the function as a method on a instance of that type. Also, there's no mention anywhere of that Input
parameter (consider how it would be harder to understand if there's more than one generic parameter).
It's not my intention to criticize Kotlin extension functions, they're great, and actually more powerful than Swift extensions (because we can't write parametrized extensions yet): but there's a clear difference in syntax strategy here, and Swift wants to look more like natural language. So, Swift doesn't shy away from verbosity if it has a chance to be clearer for a beginner.
The requirement to write parameter arguments when calling functions (if the API author wants it) goes the same way: by using certain expressions for parameter arguments, you can read a function call almost like natural language.
But this doesn't mean that everything is supposed to be spelled out all the time. For example, there's absolutely no need for a human reader to see a semicolon at the end of each statement, if there's a newline: the newline is a sufficient cue, and the semicolon is redundant. This "redundancy" problem can be found in many other places.
Consider the elision of return
for single expression functions. The reason why it can be elided, and the redundancy, is related to the fact that in the previous line (or even in the same line) the return type is fully spelled out, and the function body consists of a single expression:
func frobulate(with value: Int) -> Foo {
frobnicate(at: value)
}
This might still look uncanny (and sometime it really is, for example if the function returns a closure), but it's consistent with another important element of Swift, that has been the same since the beginning, that is, closures return automatically if they contain a single expression: this is a fundamental feature of the Swift syntax, and for good reasons (image having to write .map { return $0 + 1 }
, for some reason you might like it, but it's pointless, and causes too many characters to be unrelated to the code meaning).
I think this would be bad, though:
func frobulate(with value: Int) { frobnicate(at: value) }
Here, to know what the return type is, I need to inspect the implementation, and it's a terrible idea: I think it also terrible, and not "elegant" at all, in languages that allow for this. In my opinion, Swift should not take those languages as "good" examples: if anything, the opposite is true.
Of course, if the case was the following:
func frobulate(with value: Int) { String(describing: value) }
Then again we would have enough cues to understand that frobulate
returns a String
, but this is only because we're using the String
constructor, that requires to write String
(it's a similar story for literals): allowing for this would introduce an inconsistency between function signature requirements based on the specifics of the implementation, and it's not worth it, it's not good, it seems to me that other languages are at fault here, not Swift. This of course doesn't mean that removing the need for {}
is out of question for single expression functions: it could be done in theory, the important part is to preserve the return type.
Anyway, eliding return
is even better for computed properties:
var foo: String { bar() }
A return
wouldn't add any information there, no more than a semicolon at the end of the statement. But these things were discussed at long in the proposal, at the time.
I'd like to conclude by saying a few things on the @autoclosure
conundrum. The @autoclosure
annotation on a function parameter means that the parameter will be evaluated lazily, that is, not at the exact point of call, but when and if it's actually needed (the "if" part is the relevant one, and the reason why you, as an API author, might want to use @autoclosure
at all; I repeat: @autoclosure
is for cases when you might not need to evaluate a function parameter, given the control flow, and not just for eliding {}
). This is an implementation detail, and should not concern the API user: there's not need for cues here, because the user shouldn't need to know that information.
But there is a case where @autoclosure
might lead to unexpected behavior: when the @autoclosure
parameter is also @escaping
. This means that the lazy evaluation could happen at any point in time after the function has returned, which might lead to unexpected results. This information is certainly important, and should be conveyed to the user via some cue, but it's not related to @autoclosure
: it's related to @escaping
. This is also problematic for actual closure parameters: Swift doesn't really spell out the fact that a closure could be executed at any moment in the future, and this could be a problem.
For now, the only thing that we have is a compilation error related to capture semantics, for example:
infix operator &&&
func &&& (lhs: Bool, rhs: @escaping @autoclosure () -> Bool) -> Bool {
lhs && rhs()
}
final class Foo {
func foo() -> Bool {
true
}
func bar() -> Bool {
true && foo() // ok
}
func baz() -> Bool {
true &&& foo() // error: call to method 'foo' in closure requires explicit 'self.' to make capture semantics explicit
}
}
For this reason, I try to avoid using @escaping @autoclosure
on parameters: in case of @escaping
, I require an explicit closure, to at least give a cue about what's going to happen. But the situation could be better: I have no suggestion here, but a possible solution could be pitched in another thread.
If this is still being discussed, I think => would be a suitable substitute for -> in this context.
I have to agree with Spencer Kohan that the braces are a good visual indication of executable code, but if we want a symbol I feel it must really be |->, it would make every mathematician happy.
X -> Y indicates the sets (types) your mapping between and
x |-> y shows what happens to the element (instance) in the mapping
Just want to add my vey strong -1 on this pitch. It is both very confusing (overloading the meaning of ->) and totally unnecessary.
In addition, the type checking overhead this will add to the compiler is considerable and is the last thing we need when trying to reduce build times.
Strong -1 here. In the big picture this really doesn’t change anything. What is enabled by this change? What program can you write with this new syntax that you couldn’t before? Which problem that was hard to solve before gets easier with that?
If we want Swift to progress we really need to focus on hard problems like async/await, advanced generics, reflection and so on. Things that enable us to express more ideas directly. We need to stop the endless discussions about minor syntax details like this or trailing closures.
Realistically I feel like a hygienic macro system akin to Rust's macro_rules!, Dylan's define macro, or Honu's work on macros would be a better alternative to introducing small syntax changes.
There's precedent with the function builders for metaprogramming, why not go all the way and solve these issues in the meanwhile?
I don't know what the current plans for this are, but Chris Lattner expressed a strong desire for hygenic macros back when he was working for Apple.
I think this is of marginal utility, and while it can make things slightly cleaner looking, I value the principle of keeping patterns the same over notions of clean. I very much like a function block being extremely clear. If it's in braces, it's a function block. Also, I really dislike a generic type placeholder being omitted in the body of code, this looks like a mistake.
-1
I'm -1 on this for the reasons stated by others (->
is already defined, the new use is confusing, there's no real need for it, etc.).
However, if this proposal does end up moving forward, I also want to get my opinion out there that =>
would be a much better choice than ->
. It would maintain the same "idea" of an arrow (looking visually similar) but not be a redefinition of an existing operator (I don't think ->
is technically an operator but I'm not sure what to call it).