let entity = switch validate(post: post) {
case .failure(let e):
errors.append(e)
return .failure(errors)
case .success(let x) where x.isEmpty:
empty
case .success(let x):
logger.info("ok \(x)")
x
}
With this I'd give the pitch a +1
.
Implicit returns in single statement functions/closures is still something I'm iffy about today. Because of code like this
let task = Task {
await updateDatabase()
}
(An oversimplified example.)
Regardless whether updateDatabase()
is marked with @discardableResult
or not, the function's returned value is retained in the Task
for as long as it lives. But was that the programmer's original intent? task
's type is not explicit and the compiler does not prompt an error or even a warning for this code. It's a very, very easy (potential) mistake to overlook.
If we expand the Task
with even more functions
let task = Task {
await updateDatabase()
await updateUser()
await clearCache()
}
it becomes even more unclear whether the programmer's intent was to retain the last function's return value in the Task
. Especially for other people working in the same project who didn't write the code.
And yes, I know this is all subjective
The idea of that rewrite is interesting.
However, what I want to emphasize is that, as a general principle, early exit has merit. Just because one specific piece of code I presented can be rewritten in a different way doesnât mean the usefulness of early exits disappears.
The return
statement is often used outside the end of a function. While itâs possible to structure the code so that return
only appears at the end, such a style is unlikely to be mainstream.
In the same way that return
allows for a useful early exit from a function, determining the outcome of an expression early using then
should also be considered beneficial.
I have a question.
Is the leading dot syntax valid for multiple statements?
Also, does auto completion work?
let result: Result<Int, any Error> = if Bool.random() {
.success(1)
} else {
print("failed")
.failure(SomeError())
}
And if we ever want to consider for
or other loops for the same treatment, then even if we adopt a "last expression as return value" rule here, we'd need to invent a keyword for loops if we don't do so here. By contrast, something like then
could be that keyword.
So I'm coming around to a yield
-like keyword (maybe bind
or assign
âname can be bikeshedded later), with a subtly different alternative rule: We only support if
and switch
expressions to declare variables, assign values to already declared variables, and to implicitly or explicitly return values. In the last case, we already have explicit return
. So I think we ought to consider a keyword that's only used for assigning to or declaring variables, not returning. Just as return
exits the function, such a keyword could support early exit the most immediate containing assignment operation, and I think it's pretty readable:
let x = do {
blah()
blah()
guard foo else { bind "Bah." }
blah()
blah()
bind "Hello, world!"
}
Or we could also avoid the need for a "lesser return" keyword (then
or bind
) by introducing a modifier that would "catch" the returned value within. I'll call it eval
:
let x = eval do {
blah()
guard foo else { return "Bah." }
blah()
return "Hello, world!" // returns value to the `eval` point
}
Here, the return
sends the value to the enclosing eval
context, assigning the value to x
. This is pretty much the same thing as wrapping the do
body in a closure that is evaluated immediately.
It also works with if
:
let x = eval if Boo.random {
blah()
guard foo else { return "Bah." }
blah()
return "Hello, world!"
} else {
"Bye!" // one statement, so implicit return on this branch
}
Here, the body of the if
and else
can each return a value to the enclosing eval
context. Again, this is pretty much the same as wrapping the if
body and else
body each in their own closure.
Same for switch
:
let x = eval switch Boo.random {
case true:
blah()
guard foo else { return "Bah." }
blah()
return "Hello, world!"
case false:
"Bye!" // one statement, so implicit return on this branch
}
I guess this goes a bit against the current modifier-less syntax of existing if
and switch
expressions. Were they not already in the language, I'd suggest if
and switch
expressions be required to always be preceded with eval
, allowing you to upgrade independently each branches to use control flow whenever needed. As it stands now, you'll need to add eval
to your if
and switch
expression to be able to return
within a branch.
I'm happy to see that the discussion seems to be refocusing on alternatives, because the more I think of this (last expression as "implicit" return value) the less I like it.
I strongly disagree with the "familiarity" talk. Swift syntax is designed to be clear, explicit and readable almost like english, and the return
keyword at the end of a function has an important role, and conveys some important meaning: the fact that one could get "familiar" with not seeing that at the end of a function doesn't make it a good idea. An important piece of information is still going to be missing, and languages that omit it (without any other syntactic trick) are doing it wrong, and simply employing an inelegant solution to a real problem.
The proposal to omit return
in single-line functions clearly and correctly stated that the main issue of using return
for one-liners is that it's not needed to understand the code, because the function is so small that the meaning is going to be clear from the context, and that is still true, and it's probably still going to be true for short expressions that are only a few lines long (but it's impossible to arbitrarily decide how many lines is that). But in a longer expression, for example
let y = if x > 42 {
"yello"
} else {
let a = doThis()
doThat(with: a)
let b = doThese()
let c = doThose()
doAlsoThisButInAFunctionWithAVeryLongName(with: b, and: c)
"ok"
}
the code is just not going to be clear. Something is missing on the "ok"
line to communicate that the value is going to be returned from the expression. One could eventually get "familiar" with this, but doesn't make it good. This is of course even worse in general functions: omitting the return
at the end simply removes useful information and makes code less clear, there's no reason to do it per se.
The return
keyword has a very specific function and conveys a very specific meaning: it's used to return from the enclosing function, not the enclosing "scope", in fact returning in a for
is an often used pattern to design certain algorithms.
We need then something new to communicate the "return" from a scope determined by an if-switch-do
expression, and since it's a new requirement with new semantics, I 100% support a new keyword for it (bind
seems lovely). Also, I don't see any problem in introducing a new keyword: even in spoken languages we often have neologisms when a new concepts arises that simply cannot be expressed by the existing words. Reusing return
just for the sake of not introducing a new keyword would be, to me, a mistake, because as I mentioned, it has a different meaning.
So, overall, I'm -1
, and I think I understood why: the proposal solves the issue of multiline expressions with an inelegant solution, that is, just asking devs to remember that the last line is going to be returned from a scope, even if it's not spelled out by the code in any way, thus undermining the relationship between the Swift syntactic choices, that have always been aligned with the idea of an english-like readability, and code clarity. This kind on inelegant solution is also employed in other languages, that to me are worse for it, so I don't see it as a good argument in favor of it.
I'm going to twist this into an argument for the proposal (sorry). The intent of try
and await
is to help the reader recognize the potential for abnormal control flow at a call site that might throw or suspend. In other languages where implicit return of the last expression is normal, some coding styles encourage the use of explicit return
only in cases where the return is an early exit from the function; in particular, Rust encourages this by warning when explicit return
is used unnecessarily. A style rule like that gives the return
keyword a role that's arguably similar to try
and await
, alerting the reader that an unusual early exit is occurring rather than normal execution to the end of the function. To me, that philosophy also argues somewhat against using any other keyword to mark an implicit return, under the idea that running to the end of the function is "normal" and shouldn't need marking.
Or instead of bikeshedding a new keyword, you can use an existing keyword that is already used for early exit from blocks like break
I don't really agree with the idea that an early return from a function is any less normal than an ending return. For much of my code, an ending return is the least normal, as it's what happens when all other 'normal' options have been exhausted, so it should be more obvious if anything.
Very much this. Common example:
func first<T, S: Sequence>(inSequence: S, thatIs: (T) -> Bool) -> T? where S.Element==T {
for element in inSequence {
if thatIs(element) {
return element
}
}
nil // The "normal" return
}
In no way does this read as more clear than return nil
.
To me, this would only work in a language where all functions are single expressions, and subexpressions are not declared in the normal function body, but in a where
block, for example:
func frobulate() -> Int throws {
doThisAndThat(with: a, and: b) where {
let a = foo()
if a < 0 { return 0 } // <--- I don't expect to see a `return` here! it stands out
let b = try bar()
}
}
If everything is instead in the regular function body, I would expect to see the return
keyword in all places that describe what the function returns.
This seems to be very subjective. IMO this reads significantly more clear than return nil
, since it doesn't have unnecessary clutter introduced by a redundant keyword. The closing brace after nil
makes it perfectly obvious that nil
is the return value. There is no ambiguity about it, especially with the fact that Swift requires return types specified explicitly in functions.
Thus, with syntax that is subjective (see implicit self
), it seems natural to allow both forms for a developer not to be restricted and to choose their subjective preference as they see more fitting for their project.
(Disclosure: I personally prefer explicit self
in my code, but I don't find it necessary to impose this preference on code not written by me, unless a project specifies either preference in its formatter rules for consistency).
Apologies if this has already been asked, I wasn't sure what to search for and the thread is getting too long to skim...
What is the synxtax for guard
clauses within an if/else
expression, or are we disallowing them? We can't use return
, because it would be ambiguous and/or inconsistent, so surely we must allow guard
to support implicit return values?
let good = if badness < 1000 {
true
} else {
guard badness > 2000 else { false } // what is the syntax to propagate 'false'?
isItHonestlyThatBad()
}
I agree with this, but otoh, developers often tend towards the lazy/easy option, so we might end up with a proliferation of returns that are implicit more because of laziness then because of clarity.
There is nothing abnormal about an await
. await
demarcates points of concurrency, but concurrency is not abnormal. Control flow on the task continues linearly through the functionâexcept in the case of task cancellation, which is abnormal.
There are millions of existing lines of Swift which all use explicit return
. Clearly such a warning would not be desirable to retrofit into Swift, so the benefits this warning has brought to the Rust ecosystem do not apply.
Again, there is nothing inherently âunusualâ about an early exit. This is the âcyclomatic complexityâ idea which I thought had been thoroughly discredited by now.
I don't feel too strongly either way, but I would lean against this pitch. I worry it makes the code in question too magical/spooky. It may make it harder for Swift novices to reason about their code and understand the side effects of particular constructs.
But again, I don't feel too strongly either way, so it's a soft -1.
But then if you change âforâ above to âforeachâ, or âmapâ, etc - the change is so subtle, yet the difference is so huge⌠(although in fairness this is due to Swift using the same brackets for two very different things).
That's why I've mentioned formatters/linters multiple times here.
People preferring to enforce their subjective definition of clarity project-wide can resolve this with automation. Regardless of someone's motivation (laziness or subjective clarity), code becomes consistent with a pre-commit hook that invokes a formatter with preferred rules.
This is probably a silly idea but I thought I'd mention it, if only to perhaps spark others.
I'm slightly concerned by the implicitness of omitting the return
in if/switch/do expressions but I also find the proposed then
and variants a bit clunky.
What if in cases where if/switch/do is used in an assigment it came with a $0
parameter as a standin for the variable being assigned to which you'd use as follows:
let foo = switch bar {
case a:
$0 = 1
case b:
$0 = 2
}
I'm not sure if this would work or cover all the scenarios but I think I like it because it only uses things we're already familiar with and also mimics the withXXX
RAII pattern which is typically used with a $0 as well (i.e. withLock { $0... }
).
Not sure if it'd be a stretch but it could perhaps even support a named parameter, either by defaulting to the outer name, i.e.
let foo = switch bar {
case a:
foo = 1
case b:
foo = 2
}
or as usual by providing it, like in a closure:
let reallyLongButBeautifyllyExpressiveName = switch bar { x in
case a:
x = 1
case b:
x = 2
}
Does that make sense? Is it nonsense? Has this perhaps been suggested/dismissed already?