[Pitch] Last expression as return value

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.

No. See my post here: [Pitch] Last expression as return value - #149 by Ben_Cohen

2 Likes

I feel like this contradicts all the people who said that removing a constraint would make things easier for people new to the language. Like, they now need to learn that some expressions that work with ternary won't work with if, but might compile a bit faster [1]. It makes me wonder how long before someone pitches removing that "limitation" too, and allow bidirectional typing for if and switch expressions.

I can't help but feel this proposal is a slippery slope that largely misses the point of the original. It would create a construct that does some things more than a ternary, and some things less, requiring that much extra brain space just to figure out whether or not it should even be used in any given situation.


  1. If that's a priority. It feels like a niche thing to keep in mind. ↩︎

2 Likes

I don't believe there's any desire from the core team to make if expressions work like ternaries. If anything, backwards compatibility is the only thing keeping ternaries from being made to work like if expressions.

1 Like

I'm +1 on this proposal. This continues a very swifty trend in the language that has been there since day one and on the whole makes the language more consistent given some of the recent additions to the language.

I am against making the language more complex than it already is (or I am for slowing down the rate of added complexity), however this change is just further accentuating an existing feature of the language and applying it more consistently. Yes, removing the returns can make some code harder to manually parse, but that is the language and part of learning it. There are features in every new language we learn that break our brains a bit until we groove those neural pathways.

3 Likes

I really wish we had thought through the if statements as expressions and had gone the way of requiring an explicit marker (some have suggested something like do) but I would have been happy if it was get and return would be give

let u: Int = get if ... {
                 ...
                 give 2
              } else if ... {
                 ...
                 give 3
              } else {
                 ...
                 give 5
              }

I don't think I like 'leak' but it is sure a lot more clear and having nothing at all (which I oppose).

1 Like

If we bringing learning topic IMHO last expression as return value is more valuable for beginners as people can face it in lots of other languages, than number of keywords only in one language.

1 Like

Unfortunately that just isn’t true. I wish it was true that teaching Unexplicit things was easier than teaching explicit things. It would save me some time. I do not teach that you can elide types from variable declarations anymore. I do not teach about trailing closures at first (or maybe at all, I avoid them). I’m even careful around autoclosures when creating sample code for my students. I need them to see what is actually going on. Only after they solidly understand what the compiler is guessing, do I start to demonstrate that certain things can be removed.

Explicit code is always better, safer and easier to teach. it allows me to get much more quickly to the concepts.

Builder functions are still exceedingly difficult to explain. Especially when control flow statements don’t work the same. There is too much incredible magic happening behind the scene with builder functions.

9 Likes

I hear what you’re saying, but you’re also showing some of the limits of “what’s best for the first month of intro programming” as a rubric for language design. I completely agree with teaching your students to write out variable types, but that doesn’t mean that type inference shouldn’t be in the language at all. Teaching intro students a curated subset and then gradually expanding it as they begin to master concepts is time-honored practice.

21 Likes

Explicit is better than implicit. If even Python, the language that decided to get rid of curly braces in favor of letting the interpreter figure things out solely from indentation, agrees on that, we should probably take note.

2 Likes

The idea behind Swift has always been for me safety and clarity. Being Explicit always wins that one. When those two things are violated, it is not the "Swifty" way of doing things.

I am trying my best to refrain from having a discussion of past 'features' that were added to the language that in retrospect should have never been (either because they degrade compile times or they reduced clarity and clarity and make the language more akin to messy C than to the philosophy of Swift). I am really just trying to focus on not introducing more harm to the language. I know it's difficult to advocate for such harm reduction without bringing up these cases, but when they are discussed I think it mentally taints our bias towards "well we let these types of features in before, and I like them, so let's do this as well."

So I will say it again, eliding return causes measurable harm to the language, debug cycles, and if those are not accepted reasons (because they can be viewed as just opinion), then at least considered that it is not needed. Allowing it to be elided does not solve a problem, it encourages a problem, creates ambiguity, and may have special circumstances that create valid code when there should not be. There is no functionality or feature to be gained by allowing it to be elided.

The compiler can figure out when you need await because the function will be decorated with async. We choose to require it because disclosure of that yield point is necessary to quickly seeing control flow. This is truly no different than return . Return has consequences to what value is being thrown around. Not just reading it but also searching for it! The language should refrain for guessing what I want to do with my code.

Furthermore, I do more than just not teach them at first, I briefly cover the less Swifty way of doing it (i.e eliding things), and then move on to encouraging to never do it. Avoiding eliding code just makes for stronger code that is better read by a team and also allows you to -- for example -- debug easier in a TextEditor (which is what I do when I compile on Ubuntu).

Sometimes I wonder how many of us are working on large projects where little details like this an have huge impacts on debug cycles. I can guarantee you that working on projects with hundreds of files with with 10s of thousands of lines makes bug prone features like eliding important control flow keywords much more difficult (worse types!)-- even for code that I wrote myself, looking up return types for functions that are getting assigned to variables can be a headache (and I am guilty of it!)

Let's not continue this trend.

I do think I need to yield on this topic, however, I am just repeating myself. And even I understand that adding this features will only make it harder to find return types, make code less safe, create silly debug situations, but we are all smart and can figure it out. The language will carry on.

Thank you for all who took the time to read my position. You have been very kind to humor me.

20 Likes

Seems that it is time for summaries, so here is mine.

The new syntax allows me to inject log messages inside expressions, which is useful for debugging. Am I going to use this feature? Probably yes, with some caution, since it works in some context but not in other seemingly similar contexts. Certainly yes, when I am desperate during debugging and any additional help is welcome.

Am I going to use it as a new debugging paradigm? Definitely not. I prefer to have a sound debugging strategy and to implement it in a systematic way. The opportunity to inject some log messages inside an existing expression is not sufficient.

The new syntax also allows me to introduce side-effects inside expressions. This in turn could make the code more compact. Do I need this feature? Quite the opposite. I try my best to separate update logic from pure expressions. I had many times this great feeling of eliminating code lines without losing functionality. It was many years ago, even before Swift, and I don't miss it. Minimizing the number of lines or bytes is not my target. For this purpose I could remove comments and I could refactor variable names to a single letter. It is mechanical, easy, and very effective. It is also very counter-productive. Separation of ideas is much better than local code reduction. It allows for better refactoring at a higher level, and eventually also reduces the code size by much more than a few lines here and there.

Sooner or later I'll find Swift code with if/switch expressions mixed with side-effects. Can I work with it? Definitely yes. Even if this style becomes popular, it will be quite limited. Anyway the code cannot be too complex and correct at the same time. So I am not worried about the widespread of a feature that I don't like.

The new syntax also allows me to omit the return keyword in some places, if I want. I definitely don't want to do so, actually I'm going to fill-it in where it is missing. But can I work with code copied from others? For my personal projects definitely yes. Either I use some code as it is (and I learned to not touch something that works ok), or if I want to refactor it, I'll make many more changes than adding return here and there. In addition, the rules for where return can be omitted are pretty simple, so it is mostly a mechanical process. So not a big deal.

For team projects it is different though. What starts as a personal taste can quickly escalate and become an existential problem. If my team likes to omit return, I don't have many options. Re-formatting the code every time I want to look at it is not a solution. Going after the others and filling in returns is not a solution either. Even worse, if I cannot read code without return and my team cannot read code with return, there are only two options left. The first is to adapt to the others, the second is to find another team and project.

Overall, the last expression with optional return has the potential to create big annoyances in team projects. The if/switch/do expressions introduce features that I hardly need, but nevertheless they are manageable.

8 Likes

+1000

1 Like