[Pitch] Last expression as return value

Fwiw, your example only works because body is a protocol requirement that gets ViewBuilder attached automatically. (Edit: and is unknown to readers unless you know that fact! This is literally ambiguous.)

An even more glaring issue would come from custom variables that are used much more often.

Currently this errors:

// Error: Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
var content: some View {
    Text("Hello there")
    Text("asdf")
}

But this pitch would cause it to compile and would definitely be considered a bug:

// would return `Text("asdf")`
var content: some View {
    Text("Hello there")
    Text("asdf")
}

Another reason against this feature imo. A missed ViewBuilder or result builder has been an annoyance of beginner and experienced programmers alike, however this feature allows previously erroneous syntax to now be compiled and quite literally a misinterpretation of the original intent.

26 Likes

let’s suppose we all lived in an alternate reality called Earth-159 where we all used a programming language called Swoft!

Swoft, is a lot like Swift, except in Swoft, instead of typing out the word return, you simply type a single backslash character (\) before whatever expression you wanted to return.

so instead of writing

func makeBreakfast(from ingredients:[Ingredient]) -> Ingredient?
{
    return ingredients.first
}

you would write

func makeBreakfast(from ingredients:[Ingredient]) -> Ingredient?
{
    \ ingredients.first
}

to us in the Real World, this easy to miss, awful for readability, and it doesn’t take long to image how a Junior Dev could trip on a newline set themselves on fire and accidentally demolish your entire Company trying to edit this.

but to the denizens of Earth-159, they are used to this, have experience using the feature, and to their eyes, the \ token stands out a lot more to them than it does to us. so to them, this code is very readable, in fact spelling out the whole word return just feels gratuitous.

now we are from the Superior Earth and this strange language Swoft is just evidence of how backwards and primitive Earth-159 is! but Swift developers do a lot of things that appear dimwitted to Swoft developers too. for example, Swift developers tend to zone out and write

func makeBreakfast(from ingredients:[Ingredient]) -> Ingredient?
{
    ingredients.first
}

which is rejected by the Swoft compiler because Void is not compatible with Ingredient?.

you see, on Earth-159, the implicit single return statement was never invented at all, so while Swift developers happily spin out items.map { ($0.id, $0.name) }, Swoft developers are still typing the entire mouthful that is items.map { \ ($0.id, $0.name) }.

it’s not that the idea never occurred to them, it’s just that going from \ to just doesn’t save enough textual overhead for implicit single return to be worth it. if fact, if this were pitched on Swoft Evolution, it would probably accumulate many -1’s from Swoft users who think it is just making the language more confusing for no good reason.

now Swoft is not a Real Language, but Rust is, and Rust really does have a real life \ keyword.

in Rust, the equivalent of \ is . that is, for Rust users, it is the absence of the ; character that signifies that an expression is to be returned.

this is really fundamentally different from what is being proposed to be added to Swift. to understand that an expression is being returned in Rust, you just need to look at whether there is a semicolon at the end of the line. in Swift, you would have to visualize the entire control flow of the function.

for a single statement function, this was not a problem, because the control flow is trivial. but for multi-statement functions, this quickly becomes unreadable and difficult to skim.

with Swift, we made the decision long ago to not have semicolons, and we enjoy the conveniences that come with not having semicolons. but one of the future designs this forfeited was the ability to introduce any kind of multi-statement implicit return without dramatically harming readability.

43 Likes

I'm +1 on this pitch, as it makes the existing behavior consistent across the language and contexts. I empathize with those who dislike implicit returns anywhere, and it would be a reasonable position to want to completely remove implicit returns from the language.

But I don't see the benefits in retaining the inconsistent status quo, and prefer either going all in, or removing implicit returns wholesale. Between these two options, the former (this pitch) is more realistic and a better idea IMO.

10 Likes

Here's an example I originally posted in the SE-0380 proposal:

I think one of the biggest issues with Rust's implementation is the impact to readability when you start nesting:

fn do_a_thing() -> u32 {
  // <lotsa code>
  ...
  if some_condition { // This `if` statement is the last expression in the function and will implicitly provide the return value
    // <more code>
    ...
    match some_var { // This `match` statement is the last expression in the `if` block and will implicitly provide the result for the `if` statement
      0 => 100, // This value provides the result for the `match` statement and ultimately becomes the return value of the function - is it obvious?
      1 => {
        // <yet mode code>
        ...
        10 // A second return value
      }
      _ => 1, // Another return value
    }
  } else {
    // <probably more code>
    ...
    0 // One last return value
  }
}

The lack of explicit return makes this incredibly confusing to work out where the return values actually are. This would equally be a problem if instead of being return values they were assignments to var x = if some_condition {.
To be fair, the nesting issue still exists even for single-line expressions, but it is far less pronounced. At the end of the day it is up to the user to write readable code but I feel like Rust encourages patterns like this, particularly if you use clippy.

11 Likes

Having read the proposal, I don't understand if the idea is to suppress the return statements at all or just make them optional.

If they are optional, I would +1 this proposal, as it falls down to style / programming preferences.

If the idea to make return statement invalid to return a value on the last line, I will vote -1 for all the reasons mentioned above.

6 Likes

That’s really elegant solution to support multi-statement expressions, yet I am mostly against this feature in that form, as omitted returns increase mental load on the reader and AFAIU there is no alternative either to opt-out from the feature or use multi-statement expressions with explicit return.

The latter is true for Rust, as has been mentioned few times already. There you can write returns as usual, and only in case you prefer to have them omitted, opt-in by omitting semicolon. It is arguable how well omitted semicolon communicates this, but there is at least the distinction in the code and that’s not what you get by default. This also increases complexity for the reader in Rust, as searching for return in function much easier than for omitted semicolon.

In Swift with this change we get the behavior by default, with no options. Some might argue that this is better than duality, and this is really good argument in support. Yet another argument that this also introduces more “magic” in the code you have to understand, increasing overall complexity of the language at the very basics.

I’d prefer explicit keyword as default solution for the multi-statement expressions, even if this is less elegant in some ways. This may be a bit more verbose, but you’ll have an explicit behavior and we wouldn’t need to alter the behavior of the rest of the code.

It could be nice to have last expression as return in opt-in version, but I see the challenge to introduce this without creating some sort of dialect, so it might be that language is just better without this feature.

8 Likes

Not a big fan of this, so I explicitly return -1 :grin:

If it's to be accepted, I'd suggest to at least require return if it's used anywhere else in the function, for the sake of uniformity.

Despite that the proposal says an explicit early return draws attention while the final return may be "quiet", it's not how I read code. When scanning a function I want to know what it returns in each case; the explicit and highlighted return keywords lead me to all the places that return values. So I suggest:

func f(p: Int) -> Int {
    if p < 0 {
        return -1
    }
    1 // error: `return` is required since it's already used elsewhere
}
13 Likes

+1 for this idea, but I think maybe we can narrow it down to be valid only in if/switch/do blocks for multi-line support? For if/switch/do blocks, it's great for replacing the pattern of declaring a variable and assigning it later. However, for functions and closures, I can't really see the benefit of removing the return keyword.

And I'm curious about why we cannot use if/switch/do expression for function parameters.

5 Likes

You were faster than me :D

At first, I wasn't very happy with the implicit return of single expressions in a func neither but after using it, it doesn't make reading code much harder as it's all very condensed.

This wouldn't be the case for an implicit return in any func as the control flow has to be followed completely to understand the scope. It would be easier in Python, as the indentation is mandatory there and you can easily see where a specific block ends. But curly braces can basically be put anywhere and this

func foo() -> String {
	if true {
		return "Hello"
	} else {
		return "false"
	}
}

is allowed (and much appreciated by me) but makes it considerably harder to find the last expression of a block in a big func.

While not a big fan of the syntax, I would be ok with allowing multi-line if and switch expressions, as this is something that makes the use of these considerably more attractive, but I'm a big -1 on the implicit return anywhere. It makes understanding code much harder and I don't see the added value there for day-to-day use.

The do expression is also okay and can be of use with the same rules applying to if and switch.

1 Like

return -1 from me.

If this was restricted to if/switch/do I'd be neutral. But otherwise, this cleans up the syntax of a few patterns that I very rarely use, at the expense of making ubiquitous patterns far less clear.

I've also worked on code bases large and small in languages that support implicit returns and pretty much universally disliked it; some things do get easier, but it made the rest of the code less fluent and more awkward, requiring far more diligent reading and re-reading of unfamiliar code to understand what it did.

Caveat: I don't use Swift in my day job and the only Swift I read or write is my own, so I can stick to my preferred style.

12 Likes

Strong +1 for this! I'm glad this is finally moving forward.

I'd also argue, it doesn't make the language more complicated, but in fact simplifies it by removing the edge cases.

It's already confusing enough that we already have this for single-expression closures/functions and if/switch expressions, but still require explicit return when one more line is added (e.g. for logging purposes).

Codebases that are against implicit returns and prefer explicit return coding style are free to enforce this with linting/formatting rules, similar to what already happened to implicit/explicit self usage.

23 Likes

Strong -1.

I don’t think the very minor convenience outweighs the negative effect on readability. For junior engineers, people who interop Objective-C/C++ and Swift, and anyone who has to debug in a shared codebase, this will become a minefield.

Although it’s a bit different, I think we can look to SwiftUI for an example of how this might affect developers. Implicit return in SwiftUI can be very difficult to contend with, especially for people just learning the language or framework. It makes compiler errors difficult to parse and leads to debugging headaches the longer your function / variable body is.

Interestingly, I used to be a proponent of this (I talked about it with @David_Smith a few years ago). However I think my ergonomics-based interest in this area has quickly waned in favor of the a realization that readability is paramount. I’ve come to appreciate how Swift is more verbose than languages like Python and therefore allows one to debug visually, saving time and lowering risk.

[deleted previous post because I replied to the wrong person]

41 Likes

+1 for sake of language consistency. Also I very much like being able to have this in simple do/catch blocks.

7 Likes

UPDATE: Many/most of my concerns raised in this thread have since been addressed.

Ultimately I am -1 on this.

This proposal addresses a genuine pain point for me, but it does so by introducing several more bigger pain points.

Swift already has a small problem of ambiguity between expressions and statements but this proposal seems to turn that small problem into a very big problem.

let ternary = bool ? "green" : "red"
let ifExpression = if bool { 
  "green"
} else {
  "red" 
}

The ternary is obviously an expression because it can't be anything else. The if could be either an expression or a statement. We are trained from decades of other languages to think of if as statements, so most of us are going to reflexively assume that it's a statement. When I read this as a statement, I get confused because, this statement isn't really stating (i.e. doing) anything. Both branches have a string expression and then don't do anything with that string expression. Then I realize "Oh this is an expression." and reevaluate it as such. If the type of the expression is not explicitly declared, then I have to do some extra work to determine what the type will be (and so does the compiler).

The current design is already problematic because of that ambiguity, but in practice this ambiguity problem isn't that large because it forces you to keep your expressions small. It's an okay tradeoff because (in some use cases) it is more readable than a ternary, and it makes switch expressions possible.

But this proposal leaves the door wide open to multiply the ambiguity as much as we want. If I'm not mistaken Swift currently has this limitation:

  1. A statement can contain an expression but...
  2. An expression cannot contain a statement.

Many on this thread view #2 as a pain point, and indeed it is, but they do not realize how it is also a benefit because it limits the ambiguity of expressions and statements. But after this proposal the following will be true:

An expression can contain a statement which can contain an expression which can contain a statement... Good luck trying to figure that out.

let ambiguousExpression = if bool {
  doSomeWork()
} else {
  print("This is a debug message.")
  doSomeOtherWork()
  doSomeAsyncWork {
    "String expression"
    switch myEnum {
      case one: "red"
      case two: 
        print("This switch is an expression. But it sure doesn't look like it.")
        "blue"
    }
  }
}

Is this an expression or a statement? I can clearly see that it's an expression because there is an = if but that is a very easy detail to miss, and it bears repeating that we have decades of training telling us that if is not an expression. But even after you know it's an expression, you still don't know the type, so you (and the compiler) have to do a ton more work to infer the type.

It is not obvious at the call site what type doSomeWork() returns, or if it returns anything at all.

It is not obvious that all branches return the same type. The compiler will be able to determine quickly that the types on the branches don't match and will then throw an error. But now the human has to spend more effort finding which branch doesn't match.

In short, I'm not convinced that an expression should be allowed to contain a statement. It makes a few use cases more readable, but opens the door for far more ambiguity.

13 Likes

Got me thinking that the semicolon could come to the rescue, i.e.

let c = switch v {
    case .red:
        "red"
    case .blue:
        print("Returning blue") ; "blue"
}

that is, statements separated by a semicolon can be treated as a series where the last statement can return a value without the return keyword.

And I'm still for forcing return where at least one branch in the function already returns a value with return.

1 Like

This feels like it introduces as much inconsistency as it removes, though. If you have a function that has early returns, those will be explicit, but then at the end you'll just have a statement sitting by itself. @Ben_Cohen notes that as a good thing in the pitch intro as it calls out unusual control flow, but I'm not sure I agree.

Speaking of guard clauses and their association with early returns, would this be legal inside a function body?

guard foo == true else { "yikes" }
3 Likes

return -1 here as well; the examples of confusing code (1, 2) are pretty compelling

3 Likes

This is a good question. Would be important I think to get some concrete examples of what control flow looks like using this feature.

It is admittedly very attractive to have a feature which would "introduce" more explicit control flow by deleting obvious uses of return that are just noise, but a very good question is whether that actually falls out of the rules or if more load-bearing uses are also made implicit (which kind of has the opposite effect).

1 Like

Very much to make them optional - the same as cases where they can be omitted today. You would always be able to include them (and could potentially lint to do so on a particular project).

17 Likes

If I'm not mistaken, optionally allowing an explicit return only increases the ambiguity.

Currently a switch or if expression cannot contain a return. So if I see a return inside a switch, I know (and the compiler knows) that this switch must be a statement, not an expression.

enum MyEnum {
    case on
    case off
}
let myEnum = MyEnum.on
let mySwitch = switch myEnum {
    case .on:
        "on"
    case .off:
        return "off" // You cannot use `return` here in Swift currently
}

But after this proposal:

enum MyEnum {
    case on
    case off
}
let myEnum = MyEnum.on
let mySwitch = switch myEnum {
    case .on:
        "on"
    case .off:
        print("Now we can do other things like print.")
        return "off" // But are you in a statement? An expression? 
// Will this return a value for the expression? 
// Or will it exit a function? 
}
3 Likes