[Pitch] Last expression as return value

It is fine to have something un-named, if it is fundamental, it has been conceived early in the language design, and it fits to the overall language style. E.g., some strange language may decide to write x value where most others write x = value or x := value.

The problems start when they want to have a second un-named thing (mostly because they cannot think of a good name or symbol).

In the previous Pitch there was the argument "no other keywords, we have enough". I invert this argument: assume that you remove 10 of the many keywords and make their features un-named. How would the language look like then?

2 Likes

I think this is where our arguments with the current pitch differ, I think. I wouldn't have wanted this unnamed even from the beginning, I don't think.

Yes absolutely, readability bad, compiler nightmare, confusing change... honestly don't disagree with any of that... but already that's been called "fear mongering", so, you know, doesn't sound like those arguments are quite sinking in.

For me the big issue is that Swift makes me jump through a lot of hoops I don't have to in other languages which are annoying in the moment but make my code so much more stable and harder to hack.

Along those lines I want to be asked for affirmative positive consent before information changes scope.

Other things can be unnamed, sure. But this can't if safety matters. Can it? Where is developer consent that this information is safe to move?

It turns Swift greedy and permissive in a way that feels out of character. Rust does not do this. It has the missing Semi-colon. It disrupts my sense of Swift as a safety focused language.

Something trivial, like a colon before, idk. Something. Give us something.

I've given up on arguing against multiline because that's clearly a loosing battle, but I'd prefer it if Swift didn't take away requiring consent to move the information to a new scope. I hate that ResultBuilders allow this, but sailed ships and all.

4 Likes

I imagine that after a while, using the last expression as the result might feel very natural and not just like a "hack" for lazy people like me... Along with a certain syntax and its semantics, a certain "feeling" or mental model emerges. A stack of statements or expressions might yield a stack of values, most of which might be nothing or a discardable result, and the function "naturally" returns the top value of that stack (unlike result builders that take all values)... Such additions change your perception of the programming language, and with this different perception, some of the arguments presented will no longer be the same [updated formulation:] might appear in a different light to some people. Just a thought.

5 Likes

I'm not going to pick up my toys and run away if this proposal passes. I'll be happy for the people for whom it has made happy. I will think a mistake has been made, but I am, in fact, an adaptable sort.

I think the mental model you're suggesting will come to "feel" "natural" is a bad one for a language that values safety.

We can disagree on that. 100%. And the community will decide what it wants.

That said, I don't find "Relax. Your objections are just fluffy things that will go away. You'll get used to it " particularly rhetorically compelling, respectful or perhaps as reassuring as maybe genuinely intended.

Just a thought. Have a great weekend.

21 Likes

-1 I can’t imagine writing large functions that process a lot of data (let’s say a string) but also uses functions with @discardableResult that happen to return the same value as the function it is called in, comment out some code out (because that happens when developing) only to find out it is returning some data that some unexpected function returned midway through the function before all the comments not realizing you commented out the return value.

This last expression as return value is also going to have semantic exceptions like the body of a result builder which could be confusing.

It is so simple to understand why this would be allowed in single line closures/function bodies that are easy to debug and keep track of.

8 Likes

That's an interesting angle. Potential line of reasoning here could be:

That's of course if we'll need this feature for loops..

Another option is to collect only the first value in a loop, which makes more sense if "some_keyword_here" means "return". This option would need an extension of the syntax with an else part (see comment in previous pitch).
It would also allow while loops, which may not end otherwise. Or double loops, or more complex control structures.

Some of my concerns raised here and here have been addressed, but others remain unaddressed.

In short, my primary concern with this proposal is that it will generate many surprising side effects that will be jarring to both new and long-time users. We are implicitly returning values which changes the inferred type.

For a function this is not a big deal, since every func must explicitly declare its return type and it must agree with the implementation's return value.

For an if/switch/do expression, this is a bigger deal since the returned type need not be declared explicitly. So a small change can seem odd.

var childSafety = true

let carBackDoorLabel = switch carBackDoorButtonAction {
  case .lock: 
    "Locked"
  case .unlock: 
    if childSafety { // this is an if expression inside a switch expression. 
      "Locked"
    } else {
      "Unlocked"
    }
}
print("The lock on the back door of the car is now \(carBackDoorLabel)")

This example is obviously contrived but it shows how easy it is to abuse this. If I wrote the above, and then later needed to add print statements to debug, then I'd get different results based simply on line order.

var childSafety = true

let carBackDoorLabel = switch carBackDoorButtonAction {
  case .lock: 
    print("childSafety: \(childSafety)")
    "Locked"
  case .unlock: 
    if childSafety { // this is an if expression inside a switch expression. 
      "Locked"
    } else {
      "Unlocked"
    }
    print("childSafety: \(childSafety)")
}
// 🔴 All branches of a switch expression must evaluate to the same type. 

Even worse, if I simply change the order in this way, I get no error, but the switch expression evaluates to the type Void which would be very surprising.

var childSafety = true

let carBackDoorLabel = switch carBackDoorButtonAction {
  case .lock: 
    "Locked"
    print("childSafety: \(childSafety)")
  case .unlock: 
    if childSafety { // this is an if expression inside a switch expression. 
      "Locked"
    } else {
      "Unlocked"
    }
    print("childSafety: \(childSafety)")
}
// 🔴 All branches of a switch expression must evaluate to the same type. 

In most languages, it is not expected that simply adding a print statement can completely change the type, or worse yet, make your code not even compile!

It is also not required to explicitly declare the return type for a closure, so it is not hard to imagine that this same scenario could easily lead to surprising inferred types for closures. Except with closures, the user already has to juggle the cognitive overload of everything else in the closures type definition including the input parameters, capture list, @escaping, and concurrency isolation.

For those who are in favor of this feature, I also see the potential benefits, but I think there are a lot of new ambiguities and rough edges that it would create that we have yet to discover, let alone mitigate. At the moment, I'm not convinced that the pros outweigh the cons. I would like to be wrong.

13 Likes

As contrived as my last example was, it's worth remembering that it is very common for users of Result Builder APIs like SwiftUI to generate very deeply nested code. The vast majority of it is closures with inferred types, which is precisely what this proposal makes more ambiguous.

Ambiguity is a threefold problem:

  1. It makes code more difficult to understand for humans
  2. It makes the code more difficult to parse for the compiler. Remember. Swift is notorious for extremely long compile times, and obtuse type errors already.
  3. It makes it very easy for the human to accidentally write valid code that means something totally different than what they thought it meant. (Like the example above.)
9 Likes

It's difficult for me to imagine a scenario where this proposal doesn't significantly increase compile times. It looks like it exponentially increases the branches of scenarios that the compiler must check for. It effectively turns almost any if and switch control flow into an expression that must be type-checked and validated on all branches (no matter how deeply nested).

Do we have any system in place to check the compile time of a new proposal implementation before it has been fully accepted? If this proposal increases compile times more than 10% then that is already reason enough not to accept it IMHO.

4 Likes

I just also think it would be odd that you'd have a function with all these return statements in it, only for the last one to be elided -- for what purpose? So 6 letters don't have to be typed?

5 Likes

Note that this code generates multiple warnings:

It is possible to construct an even more contrived example, involving every branch having a discardable result. But the variable of void type does not go away.

Note also that the value will be used shortly after, and it becomes still less likely that the code will compile with the mistake going unnoticed (you might put the wrongly-typed value into an Any, propagating the error further)

The "worse yet, make your code not even compile!" comment here is a little odd. Note that today adding a print statement can do this when added to an if expression. This is the problem this proposal aims to resolve.

It does not seem like a reasonable argument against a language proposal to suggest that altering an code to be incorrect is bad because the invalid code doesn't compile.

8 Likes

It's worth repeating that Result Builders still pose a problem for this pitch, and I haven't yet seen a good answer to this concern. Today, we already have at least 3 very different paradigms that are semantically expressed with the same language-level control-flow primitives if and switch:

  1. plain-old control flow
  2. Result Builders. This looks like control flow, but it's definitely not.
  3. if and switch expressions. This also looks like control flow, but it's not (today). But after this pitch, it will also be allowed to include control flow and other statements?

At the call-site these three paradigms are often indistinguishable. Here's one of the first things you learn in SwiftUI:

struct MyView: View {
  @State private var showingText = false
  var body: some View {
    if showingText {
      Text("Now you can see the text")
    } 
    Button(if showingText { "Hide" } else { "Show" }) {
      showingText.toggle()
    }
  }
}

Which paradigm is the if using now? 2? 3?

If it's an if expression, then that's not valid because all branches have to be the same type and Text and Button are not the same type.

If it's a Result Builder (like it is today), then all the branches do not have to be the same type. So it would be valid. But why wouldn't it be using paradigm 3? If a junior dev, or a new Swift learner mistakenly thinks that it's using paradigm 3, when it's actually using 2, then how do we explain to them how to know which paradigm applies and when?

6 Likes

Yes, the print statement here is already invalid today inside an if expression. But the point is that this proposal makes it much much harder to tell if you are looking at an if statement or an if expression. This print would not be invalid today in an if expression

This is a really good callout and I do agree with you. To clarify, I'm not trying to argue that "altering an code to be incorrect is bad because the invalid code doesn't compile". What I'm trying to argue is that changing a very fundamental part of the language such as basic control flow is a very big deal, and it breaks every users mental model. It's not hard to imagine more scenarios where I could write code that looks like an if statement and would be valid if it were a statement.

1 Like

Yes absolutely. This code generates multiple warnings. And none of them would seem to hint to the user that line order is actually the problem.

I disagree – the warnings very clearly indicate the nature of the problem that has been introduced and caused them.

It seems fairly hard, because this thread doesn't contain one, despite multiple comments that they exist.

5 Likes

The syntax of if/switch expressions is quite clear. An = sign followed by if is paradigm 3. However I agree with you, more complex code can be confusing or even ambiguous. The intention is to simplify multiple choice expressions, but the reuse of known keywords makes the implementation confusing.

So there's a problem with my earlier code example:

struct MyView: View {
  @State private var showingText = false
  var body: some View {
    if showingText {
      Text("Now you can see the text")
    } 
        // 👇🏼 🔴: `if` may only be used as expression in return, throw, or as the source of an assignment
    Button(if showingText { "Hide" } else { "Show" }) {
      showingText.toggle()
    }
  }
}

It yields the error 'if' may only be used as expression in return, throw, or as the source of an assignment. This error is very helpful, and I wasn't aware of (or must have forgotten) this limitation. This limitation does greatly reduce the surface area of potential ambiguities. Which is good. As @georgeef says "An = sign followed by if is paradigm 3 [an if expression]."

Assuming this limitation is never lifted (say if someone in the future wants to remove the inconsistency between ternaries and if expressions) then I'd say that addresses most of my concerns.

1 Like

With another keyword instead of if, your example could be valid and meaningful. With the keyword if and without any other syntax help, it can only be confusing.

FWIW my personal preference is not to extend if expressions to appear in all positions. Ternaries are better for most of the desirable cases, others would be clearer broken up, and if anything, multi-statement if expressions make them less suitable to be used in e.g. argument position.

4 Likes