[Pitch] Last expression as return value

Big +1 from me

1 Like

Accepting this proposal would simplify/consolidate the existing rules and exceptions, yielding a simpler syntax for newcomers. Currently, you have to say: Last expression as result is possible, but only in this context, that other context (unless you want to also do this additional thing, then you have to use a different syntax altogether), and that context.

if/switch already work without any additional keyword, but with only a single expression. Introduction a new keyword would be in addition to the current situation, making the syntax more complicated.

having to make a case block an immediately-executed closure just to stay within the restrictions of the switch expression grammar feels to me like a pretty embarrassing place for the language to end up.

It is pretty awkward. However, before switch expressions, this would be expressible as:

let value = {
  switch someEnum { 
    case .someCase:
      /* complex logic */
      return thisCasesResult
  }
}()

So this inconsistency is, at least in part, due to the switch expression rule itself, as without it, the closure could alternatively wrap the entire switch statement.

1 Like

Yes, but we’re not going to revert that proposal, so this is the language we have now.

3 Likes

The fact that it "doesn't allocate a closure" doesn't change an async function to a sync one, nor does it remove the need to capture any local variables used inside it. By contrast, a block inherently has the same async/sync context and access to the same variables as its containing function.

"That ship has sailed" is a bit different from "no real objection".

And introduce a bunch of new ones, like

At least with one line, you don't even need to explain why you can't use guard, since there's no way to use guard and return a value in a single line anyway.

Overall, I think it adds up as more rules, not less.

That is an existing rule.

1 Like

this looks very good, but only because each branch is either one or 2 lines

if a branch had many lines instead, it just wouldn’t read clearly, but if there was a keyword (like bind) on the expression returning the value that is bound to goForWalk it would be much better; also, it would allow for guard or if with no else to be used in the branches… it would basically allow for the usual control flow structure we know and love

in terms of raw added complexity, adding a keyword is going to complicate the language less than just “remembering” that last expression is bound to the variable: the latter will simply not read well for long branches

3 Likes

As I've said

So, it's not a "rule", it's just that there's no way to write the code in which this "rule" is "violated". This "rule" is simply a side effect of the "only a single line" rule, and therefore needs no extra explanation.

Background: I have been teaching programing for 13 years now. It is possible that you have been doing the same, and you just don't see the issues that I have. I accept that we can have different experiences when it comes to 'newcomers' and 'new programmers'

That is simply not true. The existing rule comes out of an affordance given to @autoclosure that felt right because it simplified single line entires that were really easy to debug, albeit sometimes confusing especially when all you were writing was <. (This is NOT easy for newcomers btw)

Yet, these contexts are so very simple, you would never have pages of code to scroll through to find the return value. You would have exactly ONE line to deal with that that line alone could be the only culprit. This affordance was allowed because it sort of matched what was already happening inside parameters.

It ways always a ay of making closures within parameters kinda behave like the expressions.

You would, for example, never have to say

doThisThing(x: return 1, y: return 2, z: return 3).

Again, these would always be single lines anyway.

So then it was asked that we extend this to all single line closure (I believe).

I kinda objected to this, I think, because it served a sort of philosophical niche in parameters that didn't NEED exist outside of that. I think if Swift was made today, I would have never invented autoclosures as it hides too much of what is actually happening (this is unsafe in my opinion).

But it wasn't a big deal. It was ONE line. There really could be no major confusion about what was being returned.

Single line, allowed. Multi lined, not allowed. Very simple to understand, very simple to keep straight for newcomers.

But not always, because newcomers and new learners of programming often confuse that a body of a function and closure are different than a body in if-statement (this is about to get mudded even more). Why for example, in the body of an if expression can I not simply state the return value and it would early exit? It LOOKS kinda like a closure -- but it's not.

What if it were allowed in if-expressions? Do now all of my @discardableResults become a potential return value?!

That is absolutely dangerous.

What if it were allowed in multi-lined closures/functions and in the course of development I comment out my return statement and now a unsuspecting discardableResult becomes the return value? In the past, the compiler would warn me that I didn't write 'return '. Giving me clear notice that I need to pay attention to what comes out of this function.

What happens if I continue to develop a function, not notice that I had already returned, and I continue to type code.

I have been teaching Swift for 13 years now to young adults. I am honest when I say this, Swift is starting to become a programming language for experts. This is another step in the wrong direction of creating a language that is SAFE, explicit and easy to read.

When I teach programming now, there are things I have found are better to NEVER mention until much later in the year. 1) implicit types 2) trailing closures (all my example code puts the closure inside the (). 3) eliding return in single line closures (with some exception). They very frequently leave the students confused. It is always very difficult to explain why result builders do the funny things they do with if statements. But each of those are far less dangerous that was is being proposed here. This actually causes the program to yield values in functions that can be passed down through a call chain. In functions that are hundreds of lines long, this is unacceptable.

So please, I beg people to listen to the naysayers who are advocating for something to really be thought about in practice. Not with the brains that we have. We read code all day. We see these discussions and participate fully in the debate of how a new idea will be implemented. Even when we dislike it we will easily understand how it works.

This pitch is a decoration (not a feature) that would only exist because it feels a particular way. Not because it is needed. Not because it is safer. Not because it makes for a measurably better language that enhances the final result (the built program).

For those who feel that this pitch creates consistency despite at the expense of clarity, okay, than I prefer to be inconsistent. Some inconsistencies deserve to remain.

This pitch creates MORE confusion and more rules not less especially when we considered all the other side effects and exceptions that may need to be carved out. And the very fact this is decoration is not needed. We need features for the language that encourage safety not decorations for the language that encourage being less explicit.

24 Likes

I think there would be serious downsides to omitting any version of the then construct entirely - we probably want users to be able to return early from block expressions, such as in guard statements, as they do in immediately-executed closures. If the then construct is indeed necessary, the question becomes whether or not we should permit last-expression returns in addition to the then construct. It is worth noting that if last-expression returns are permitted, then the then construct can probably afford a more cumbersome syntax that avoids a new keyword, such as something similar to the Rust syntax: 'label: { break 'label value } .

I think the claim that permitting last-expression returns results in easier refactoring is overblown. In the general case, adding a side effect or variable binding before an expression would still require cumbersome refactoring: a bare expression would need to be wrapped in a do block. It is only when the expression in question is already the sole expression of an if branch, switch case, or function that the addition of a do block would become unnecessary; in that case, I think the lesser cost of adding a then keyword is acceptable.

In contrast, in C, adding a side effect before any expression is lightweight, only requiring the addition of a comma (sideEffect(), value in C is equivalent to do { sideEffect(); value } in this proposal). I think the approach taken in this proposal and in Rust is better, because (in addition to allowing for variable bindings) it makes the code easier to read, despite requiring more syntax and more refactoring work. I think an explicit then construct is better for similar reasons; generally, sacrificing some refactorability for explicitness is a good tradeoff here.

1 Like

The last-expression rule, as proposed, would also apply to functions. So I don't see how any additional refactoring could be required under this proposal. And without this proposal, the addition of a do block would only be required if a catch is needed.

Having said that, I don't oppose the addition of break {label}: expression, but I don't want it's use to be mandatory.

What I mean is that, for example,

let x = expression()

would need to be refactored into

let x = do {
    sideEffect()
    then expression()
}

So in general, adding a side effect or variable binding before an expression requires the addition of a do keyword and braces. It is only when the expression is directly situated within an if branch, switch statement, or function that that level of refactoring would be unnecessary:

func f() -> Int {
    // No need to add a `do` block here
    sideEffect()
    return expression()
}

I think if adding a do keyword and braces is acceptable in the former case, then adding a then or return keyword is acceptable in the latter case.

2 Likes

Discardable results are a strange thing, maybe a warning can be added if it determines the type of a closure (@ben_cohen)? (You then would have to add “return ” or “_ = “ to silence that warning.) Just as with autoclosures, discardable results should maybe only be taught later in an introductory course, at least if there is no such warning. The promise with Swift was never to be easy, but to follow the principle of progressive disclosure.

However, experiences with teaching beginners are valuable as a feedback.

1 Like

I get the sentiment. But this example does not work. The proper refactoring for this would be:

sideEffect()
let x = expression()

You only need to add sideEffect inside the same block as expression, if expression is already inside an expression-like if or switch, i.e. it only happens in one possible branch of the code. The other possible case would be if expression is in a ternary:

let x = condition ? expression() : value

But then to add something that happens before expression, you need to refactor the whole thing into an if, because you can neither multi-line nor then [1] inside a ternary.

At the same time, the ternary example makes for an interesting question: Are expression if/switch supposed to be more powerful than a ternary? Is this an intended purpose for them, to have ternary be explicitly weaker? Or was the original purpose to allow long cascaded ternaries like a >= b ? a > b ? 1 : 0 : -1 to be replaced with something more readable?

Because no one would be wondering "Why can't I add a print("oh oh") to one branch of a ternary". Or, if they did, it would involve a very different design.


  1. I really prefer break, leaning towards break = value at the moment. I think one of the biggest objections to the then proposal was the introduction of a new keyword, not the actual usage thereof, but I didn't read the whole thread. ↩︎

Like a few other people, my reaction to this pitch is “I don’t like it”, with a hard time articulating exactly why. I think my core objection is that when reading code, I like there being some visual marker to say "this is where the return value is; this is what you need to pay attention to". return keywords explicitly provide that; in a single line closure I think it's obvious from context, as it is for if or switch expressions with single line returns; and in a language like Rust it's (subtly) denoted by the lack of a ;. What's proposed here is using only the position of a line of code amongst potentially many, and to me that just doesn't indicate that something is a return value.

To propose an alternative: I think my preferred solution to that would be to burden the lines that aren't the return value, similar to what Rust does with the semicolon. In Swift, that would mean that if we say everything in a = switch, = if, or (maybe) = do is an expression, you'd explicitly mark the lines which you don't want to return the result for with an _ = . To pull a couple of examples from this thread:

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") // otherwise you'd get an error that the line is returning 'Void'
     4
}
let carBackDoorLabel = switch carBackDoorButtonAction {
  case .lock: 
    "Locked" // returns
    print("childSafety: \(childSafety)") // error: wrong return type, unreachable code
    "Locked?" // warning: unreachable code
  case .unlock: 
    _ = print("childSafety: \(childSafety)") // prints
    if childSafety { 
      "Locked" // returns
    } else {
      "Unlocked" // returns
    }
    print("childSafety: \(childSafety)") // error: wrong return type, unreachable code
}

In other words, the rule would be that in expression contexts, anything that is an r-value (i.e. not a _ = , let _ = , or func declaration) is returned as a result of the expression. To my taste, those _ =s give enough of a visual indicator that "something is different in this context; the unmarked lines must be expression return values" in a way that the bare last expression just doesn't.

5 Likes

I believe one of the motivations for if expressions was to discourage the use of ternaries as much as possible. Not merely to improve readability, but to reduce the amount of expensive bidirectional type checking.

1 Like

The inferred type of

let x = {
  // some code without return
  return optional?.voidMethod()
}()

depends on the presence or absence of return ('()?' or '()', resp.). What is the type with the last expression rule, if return is removed?

1 Like

Isn't the bidirectional type checking necessary for if expressions as well?

Yes.

Introducing leak, an expression marker, used in blocks or switch statements that yield a value.

Since yield is off the table, the next alternative that comes to my mind is leak :grin:

let u: Int = if ... {
                 ...
                 leak 2
              } else if ... {
                 ...
                 leak 3
              } else {
                 ...
                 leak 5
              }
let u: Int = do {
                  ...
                  leak 2
                }

let u: Int = repeat {
                  ...
                  leak 2 // break with a value
                } while true

let u: Int = for i in 0..<32 {
                  ...
                  leak 17 // break with a value
                } 

Any statement that follows a leak triggers a warning or error message.

let u: Int = for i in 0..<32 {
                  ...
                  leak 17
                  print (i) // Warning - statement after leak has no effect
                }
1 Like

This is similar to my proposal. In all examples leak is the last expression, but it can be more general. You can have guard, inner if, inner loops, etc., which contain leak. Essentially you can have leak in any place where return could appear in the body if it was a closure.