On the topic of do-expressions. They might replace immediately executed closures, so maybe that should be considered. Would immediately executed closures still offer anything useful if this pitch is adopted?
let val = {
var i = 0
return i
}()
Is the same as:
let val = do {
var i = 0
then i
}
Except the later can also handle exceptions.
let val = do {
try randomlyThrowException()
} catch {
fatalError("You are unlucky.")
}
I suppose closures would still be needed for lazy, but that feature could be altered in a major language mode to simplify the language.
EDIT: "defer" and "guard" might be used in immediately executed closures that wouldn't translate to do-expressions, but I think do-expressions would probably end up more popular which might not be a bad thing since they are less ambiguous and easier to parse.
I think if I could change one thing about Swift retroactively, it would be new syntax for applying closures returned from a variable or function. Maybe myClosure.("an argument") with a dot to apply a closure stored in a variable. There is just so much ambiguity in that for a language without required semicolons.
I don't think they need to be removed. It'd feel like an oddly arbitrary restriction, for something that naturally falls out of the basic language syntax & grammar. Though there could be a FixIt as a suggestion to convert them to the more canonical form.
But otherwise I like the direction. Mainly because it'd be really handy to have some sort of concise shorthand for tryâŚcatch, which your proposal is one baby step closer to.
It is possible there are other compromises, but I think any of them would cause parsing issues or just plain slow parsing, IDE autocomplete issues, etc. because the parser will need to look ahead (or behind) so far.
The problem is you don't want anything that results in this "(...)(...)" except for two statements next to each other.
EDIT: I think another option would be to require that immediately executed closures can't have parameters. Nobody uses parameters with them anyway.
In other words, don't allow this:
print({
return "Hello \($0)."
}("World"))
There might still be some issues with void types, so it wouldn't be ideal but probably workable.
All this said, immediately executed closures is a parsing minefield so it might be nice to replace them with do-expressions during a phased process with fixits. They are too similar to keep both features long term or it will be confusing. They are not 1:1 comparable, but nobody uses the closure specific features. I don't know if there are ABI issues, but the compiler can always translate to a closure under the hood and would expect that to be rare too (maybe with global variables).
Mmm⌠I agree it seems odd and suboptimal, but I wouldn't want to jump to conclusions. Especially once one gets into a debug mindset or of "ugh, why is everything not working, sigh <shift more random code around in frustration>", I wouldn't be surprised if one can come up with situations where that makes a kind of sense. At least as something useful to do temporarily.
If there are indeed significant parsing challenges (or slow-downs) as a result of the syntax, then that's one thing, but removing natural syntax for purely stylistic reasons seems overly zealous.
If one form truly is naturally superior to the other, then it'll inevitably become dominant in use and the zeitgeist and there'll be no need to forcibly ban the other - convention will achieve the same result. Alternatively, if neither gains a natural dominance then that suggests there's merit to both, and they should both remain.
Yeah. There is some change in behavior with âreturnâ and I can think of a few things that would not be clean fixits. Namely âguardâ and "defer" and what to do about that would be a whole discussion in itself. Probably not a path to go down anytime soon since they could be kept around.
I could see at least making immediately executed closures more awkward and less ambiguous to use inside functions (like wrapping in parentheses) since do-expressions would be more ideal. It could just be a recommendation that becomes required in Swift 6. No change to immediately executed closures outside of functions. That would provide a better migration path that could be maintained indefinitely and fixits would be easy to write.
EDIT: I deleted the post this is referring to because it looked nice on the surface, but had issues that are hard to write up. I don't think there is a way to make implicit return work well without semicolons.
Still relates to my post about do-expressions above. Exploring this more, I think I am still a fan of this pitch. I think it is necessary, but there might be room for implicit return with semicolons and statements containing clear keywords that could be explored in another pitch. It is too much to bunch in to this one. I just don't see anyway to avoid an option for explicit return without major changes to syntax or leaving expressions feeling half-finished. This is a feature that could be avoided if you have a personal distaste for it since it only shows up in implementations. Much like trailing closures which are fine to use or not and still feel Swifty.
Does anyone know what Full Expressions are under SE-0380 in future directions? It links to a pull request (hamishknight:express-yourself) that doesn't exist anymore. I wanted to see how this proposal relates to future directions in SE-0380.
EDIT:
Nevermind. I see they are nesting if/switch/do inside another expression. I think this pitch gets part of the way toward that if you break up the expression a bit. There is at least a limited form of nesting in this pitch and mixing in let bindings is a big step toward more complex expressions. I'm not sure if we even need more than this since the Full Expressions proposal has a lot of overlap with higher-order functions.
Full Expressions may still be interesting even though they have a lot of overlap with existing features. They almost feel like building a list comprehension in complicated cases mixed with a loop. They also don't appear to require "then" in common use, so that might be enough for anyone that worries about mixing "then" all over their expressions.
In relation to Full Expressions, Multi-Statement Expressions feel like mixing a little bit of imperative code in to an expression instead of a "Full Expression" which feels like a functional language. I think an explicit return using "then" works nicely for that. I didn't really get the connection until I looked more in to the Full Expressions future direction which are more in line with what most people think of as an expression.
Are these ever used in this way (or will be with new ownership model)? I would think it odd to do this in an anonymous immediately executed closure, but I haven't worked enough with the new features. I've never seen arguments passed in any code in the wild.
Regardless, this related to an idea for implicit return that I since deleted above. I thought we could get close with minor syntax changes in Swift 6 but there were still some thorny areas.
I wasnât going to comment because I like the idea and had little to add, but since thereâs been a lot of pushback against then I think itâs worth saying that I think it works well and makes a lot of sense.
In English, âthenâ has a number of meanings. One is to express a consequence: if A, then B. Another is to express temporal succession: A happened; then B happened.
Both are relevant here. Consider an example:
let numberOfCatsPresent = if vacuumOn {
print(âđ!â)
then 0
} else {
print(âđ¸â)
then Int.random(in: 0..<4)
}
Notice that then flows semantically on two levels, with two related meanings:
For the expression as a whole: If vacuum is on, then zero cats. (consequential meaning)
Within the curly bracket sub-expressions: Print the kitty, then resolve this branch of the expression to the given value. (temporal succession meaning)
This is not just to overanalyze the grammar. The point is, then reads naturally both if you start at the if condition and then skim over the intervening statements to see what the expression actually evaluates to (ifâŚthen) â and also if youâre just looking at a single branch expression within curly brackets (statement, statement, then result).
It's true, it is technically redundant from that angle. But I think @cmonsour's observation is astute. I hadn't even noticed that 'then then' redundancy, through all this discussion of the proposal - I suspect others likewise had not, since nobody pointed this out 'til now. So it seems like in practice using the then keyword like this reads naturally.
An alternative way to rationalise it is that the angle brackets are not synonyms for 'then' and 'done', but rather just border markers for a basic block. I think this makes sense in particular because otherwise those closing brackets are hard to explain:
let numberOfCatsPresent = if vacuumOn then
print(âđ!â)
then 0
done else then
print(âđ¸â)
then Int.random(in: 0..<4)
done
I mean, it sort of works as English, but it's clumsy.
I think there's a reason no modern languages use words for these things, rather than punctuation. About the only living language I can think of that still does is Bash, if you can call that living.
I wonder why return isn't used here instead of then?
And in this code return can also be used:
let width = switch scalar.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default:
log("this is unexpected, investigate this")
then 4 // why not use `return` instead of `then` here?
}
Part of the original motivation for SE-0380 is to avoid the overloading of return as currently happens with the immediately-applied-closure pattern. It requires a reader of a return statement to remember the context theyâre in in order to understand whether weâre returning from the entire function or just a sub-computation.
I for one much prefer the use of a keyword here and am not compelled by the arguments that we should try to reuse an existing keyword. I donât think we have any sufficiently-similar keyword available today that we could reuse without running into the same problem weâre trying to avoid in the first place with return. IMO then reads fine.
I am mildly concerned that introducing this feature will cause users to introduce arbitrary side-effects in value-computing code, but I think the benefit of being able to break a branch down into multiple steps rather than a single monster expression outweighs that concern.
If we have an if/switch expression and or one or many of its branches contain multiple statements, would you not consider the whole thing to be a âmonster expressionâ of the same caliber or higher than what youâre imagining any one of its branches might be?
I certainly donât think it would be impossible to write if expression which would be better off having some of its branches (or indeed, the whole thing) pulled out into separate functions.
However, the current regime pegs the answer to âwhen should I factor things out into a separate function?â at âwhenever it is not possible to express a branch as a single expression,â and after working with if expressions in the language today for a while I donât find that satisfying. I have found myself compelled to shoehorn things into single expressions when I would really prefer to have bindings broken out into intermediate steps. And if thereâs a value that needs to be used in two places, the current regime actually encourages writing more fragile code (duplicating the subexpression) rather than pulling it into an intermediate binding for reuse. (IMO if expressions themselves are structured enough that I donât consider them âmonsterâ the same way I feel about, say, long operator or function-call chains. Or at least, the threshold is higher.)
Iâve found myself coming to the view that these if branches/expressions should be factored out in a more judgement-based way, that is, precisely when the alternative becomes more readable than leaving things as-is. I donât believe the current restrictions approximate that boundary closely enough to leave them in place. And further, I think that pushing people towards the alternative of, say, an immediately-applied closure is worse than allowing the insertion of additional statements in a branch.
I'm not big fun of adding keywords (Swift got too many already), but if to use a keyword at all instead of a bare last expression â consider using out, in addition to in and inout we already have in the language.
Fair enough - Iâm becoming increasingly convinced of the pitch overall, including the specific choice of the keyword then.
However, there is one other alternative that I'd like to bring up before this ship sails.
This problem is of course solvable in normal functions, but I don't find it to be the best experience. Specifically, I find that having to declare all of my subcomponents before the final expression severely inhibits readability, because it is sort of the opposite of progressive disclosure: instead of getting to glance at the final result and look further down only if I'm unclear on the definition of one of the terms I see, I have to process the names and definitions of all the intermediate values first and then finally at the bottom I get to see the highest level expression.
Rather than making each branch of if/switch expressions basically like slightly differently flavored function bodies, it might be nice to provide a better solution to the problem of intermediate definitions that can be used in both contexts, but that doesn't require, for example, the introduction of a new return-adjacent keyword.
Inspired (again) by a post earlier in this thread by @esummers , here's a first draft of the idea:
let accessibleRepos: [Repo] =
switch signedInEntity {
case .companyMember (let employee, let company):
employee
.personalRepos
.appending(company.publicRepos)
.appending(
isAdmin ? company.adminRepos : []
) where {
var isAdmin: Bool {
company
.admins
.contains(employee.id)
}
}
case .individual (let user):
user.personalRepos
}
The idea is more or less that expressions can be followed by where and then curly braces within which functions and computed properties can be added. The order of the declarations doesn't matter, and cyclical dependencies can be diagnosed the same as the unordered declarations that appear within types/extensions or at the global scope.
I was about to suggest out as well here a few days ago as I felt it would read better than then.
However, I didn't like how in is the keyword for defining closures, and out - as a false counterpart - would be a keyword for expressions.
Also, I have to say, after initially feeling quite positive about this pitch (with a rather firm opinion that a keyword would be a bad idea and bare expression is the way to go) I find myself more and more disenchanted.
I still think even the best keyword feels foreign and weird for this function, but I absolutely sympathize with the reservation against "bare expression returns". In order to stay consistent swift would have to go "all in" on bare expression returns - and I am not sure myself if I would want this (just as the proposal predicts ; )
I find myself more and more in the "it's not worth the fuss" camp.