[Pitch] Last expression as return value

Thanks for the hint, but with the closures you can add a “return” exiting the closure, not only for that, closures to be used for control flow are inapproriate.

That part I did not understand at all, a “do” section is not a closure.

The one case mentioned in the pitch stems from a “guessing” of the type of a closure that to my opinion also today should not exist without a warning, I opened an according issue. I did not notice any mentioning of another source break in this thread.

1 Like

Implicit return in this example may theoretically break code.

See my last answer. Update: What I mean is, it is the same case, nothing different.

I have seen it, I just give an example.

Thank you, @Ben_Cohen. I have enjoyed reading your long post.

But, actually, I find the following code hard to read, even though it is relatively tiny.

It is all because of the missing critical return.

A return before the if would have made the code much easier to read, because the return serves as an important sigil, setting up the expectation that the if statement emits a value.

func hasSupplementaryViewAfter() -> Bool {
  let cellIndex = elements.firstIndex { $0.elementCategory == .cell }
  return if let cellIndex {
    elements[cellIndex...].contains { $0.elementCategory == .supplementaryView }
  } else {
    false
  }
}

Also, because short writing makes long reading, I would revise the code as follows. :slight_smile:

func hasSupplementaryViewAfter() -> Bool {
  let cellIndex = elements.firstIndex { $0.elementCategory == .cell }
  let v = if let cellIndex {
    elements[cellIndex...].contains { $0.elementCategory == .supplementaryView }
  } else {
    false
  }

  return v
}
13 Likes

I know the ship has sailed, but return if always forces me to do a double-take. It looks far too similar to Perl and Ruby’s trailing conditions or Python’s list comprehensions. else makes this especially problematic:

return if foo { 0 } else {
  // if you are familiar with languages with trailing conditionals,
  // it’s reasonable to read the “if” as binding more tightly to the “return” than to the “else”,
  // in which case this else block would execute *instead* of the return.
  otherFunc()
}

// does this execute? Swift says no!
print("hello")

I’d have preferred if the grammar forced you to spell this as return (if let cellIndex { … } else { false }).

In fact, does mandating parentheses solve any of the problems that people are suggesting do for?

6 Likes

Although I like the look of “return (if let …)” (although it does not seem logical to put those parenthesis) I judge the introduction of the use of round parenthesis in the spirit of some constructs proposed above a bigger step and a step harder to learn and explain than the “last expression rule” and code might not be easier to read. This also applies to according uses of “do”, but I think the parenthesis make it even harder to digest. (And note that “do” has the feature that variables defined inside it are invisible to the outside, something that is problematic for what this pitch wants to achieve, so the parentheses should not shadow any variables? This would be a completely new construct, would not do this.)

Yes, this similar the source break cited in the proposal. The potential for source breaks such as this is the reason it might be appropriate to introduce this change under an upcoming feature flag. However, it may be that this particular case it could be mitigated by not applying this rule to optional void returns.

2 Likes

If the problem with @discardableResult determining the type of closures requires using an explicit return to clarify the intent is to return the discardable result, then it wouldn't be possible to use a @discardableResult function at the end of an if/switch/do expression unambiguously either, and writing an explicit return there is not an option.

IMO both this and the inability to use guard (or other "unusual control flow") inside expressions are solid reasons for having a keyword to explicitly perform variable assignment inside an expression, even if last-expression-as-return-value is accepted and used for the majority of the use cases.

4 Likes

No, you can make clear what the indent is, it is clear e.g. with “let x = if …”, and it is also clear when other paths are not discardable. Please give examples if you think otherwise. (Edit: I made it now clear in the linked issue that in some cases some refactoring is necessary to silence the according warning. This should be seldom. Concerning this pitch: If you do not get such a warning before, then the implementation of this pitch does not break code.)

Following this thread is quite the journey already with so many arguments being made.

I am pretty much in the strong -1 camp for this feature for many of the reasons posted above concerning ambiguity and code being a lot harder to visually parse and reason about except for short or trivial cases, as well as certain styles of writing logic seemingly being implicitly favored by this feature.

In the end this feels very much like a feature that Apple really needs to make an upcoming feature look nice, so it is being argued for with a lot of effort (don't get me wrong, many of the arguments are totally reasonable, and I also do know that this is not a democratic process and that has to be fine), but by flooding this thread with arguments, it is now very very hard to actually see, if the community mostly dislikes this addition to the language and would rather see it shelved. This starts to feel frustrating especially when remembering similar opposed features having been pushed through lots of opposition.

I think if there was some form of poll, and we would see that actually quite a lot of people like this change, most of the frustration would dissolve. It's usually just subjective perception of the process that triggers this.
If I am being honest with myself, I can probably live quite well with this language feature if I get the sense that most people like this, but I feel frustration if I feel that so many people are just being overruled. I suppose that's just normal :man_shrugging:.

I do appreciate the many thoughtful comments and discussion in such threads though from both sides.

15 Likes

I think that's the key point.

Regardless of one's personal preferences, it is definitely true that for very short code branches (one-liners is best, but also 2-3 lines is fine) the return keyword doesn't appear particularly necessary at end of the branch, because the fact that a value is being returned from the expression is clear from the immediate code context. The canonical example of adding a print in a case: branch of a switch shows this really well.

But it's just not true that return is also not useful for long code branches, with several statements: the return there is useful, serves a purpose, it's just not true that it's "useless" and "noise". One can get "used to it", but it's always still going to be useful, as it would also be useful to have different keyword to express the returned value from a multiline expression, in order to be able write proper structured code, with early exits, as we do in every other case in Swift.

I mean, today we can write this perfectly fine code, that uses excellent control flow techniques that are clear to understand and are compiler-aided:

func foo() {
  let xs = [1, 2, 3, 4]

  for x in xs {
    doSomething(with: x)
    guard x > 2 else { continue }
    print(x)
    doSomethingElse(with: x)
  }
}

// will print 3 and 4

Without a new keyword to bind the returned value from an expression, for example a do expression, I wouldn't be able to use guard in there, and this is inconsistent. It's not "inconsistent" to be able to omit return from a one-liner compared to a 100 lines function, because those are completely different cases, and there is a very good reason to omit return from a one liner (or even a 2-3 liner), while there is no good reason to omit it from a 100 lines function.

The argument that a new keyword would make the language more complex looks like a misunderstanding of the word "complexity": the complexity comes from the use cases themselves, it's essential, and the language is just there to express that complexity in code, and it can do it in good ways and bad ways. Specifically, I think that:

  • a new keyword will be clear, explicit and will allow for proper control flow in multiline expressions;
  • considering the last expression as return value will be useful in a limited number of cases, but will not allow for expressing full control flow in multiline expression, and will make longer and more complex multiline expressions (and functions in general) harder to understand.
14 Likes

Good argument.

The point is (not to speak of the refactoring issue), not having to add the “return” at the last expression can give you the same “feel” when you have some ceremony before that expression, I am referring to the “nice” examples where I like this very much. And we do have the choice to put the “return” where this seems more appropriate.

(BTW and off-topic you can only give a “heart” to a comment which I think is commonly understood as an “approval” consent, but for the last two comments I would only like to express that I like those comments although they express a different opinion than mine.)

2 Likes

I rephrased, hopefully making my point clearer.

I do agree with this, which is why I was initially conflicted.

I would like to elaborate on another aspect, on which I commented only briefly above: implicit return is optional.

As a colleague of mine told me once (for hardware design, but I translate to language design here), each option is a missing decision by language designers. return is one of the most often used keywords. The option to make it implicit or explicit does not add new possibilities to the language, it only adds a new style. It is not like having both for and while, it is more like having braces around single statements or not. (BTW, brace-less statements are considered harmful today, but they were elegant in the past, and many still use them today).

I go one step back, and I ask a hypothetical question, as if no ship has been sailed yet: return at last expression can be either implicit or explicit. We decide what is better and we offer only the best choice. What is the decision?

Having one more styling option of course gives more flexibility (which is a word with good connotation), but at the same time this kind of flexibility drives towards a babel. My personal opinion is that we don't need this kind of flexibility. In the long run it only creates schools of style, without essential difference in content, and it makes exchange of code and ideas more difficult. As commented already by others, many more keywords and symbols can be optionally omitted in the language. Does this make the language better?

8 Likes

Yes. Otherwise at times (using a German idiom) you can't see the forest for the trees.

This happens anyway. The big question is, does this go too far with this pitch. I do not think so, but others think differently.

The idiom may be of German origin for all I know, but it has been in use in English for at least half a millennia. It is documented in 1546 by John Heywood, in his collection of English proverbs.

4 Likes

Exactly, that is the point. return is used virtually in every function, on average maybe a little more than 1 per function. For me this is too far.

1 Like

You argued that the type of the closure is ambiguous here (which I don't fully agree with[1]):

@discardableResult func g() -> Int { 
     doSomethingElse()
     return 1 
}

let closure = {
    g() 
}

And your suggestion is that the compiler should emit a warning to enforce writing return g() explicitly instead of just g() (to make it clear that closure's type is () -> Int and not () -> Void). But then this:

let x = if condition {
    g()
} else {
    // ...
}

Must be considered ambiguous too (it's also doing type inference based on the result of a @discardableResult function). Yet it can't possibly be made explicit by writing return g() instead of g(), as return can't be used inside expressions. You'd need something like assign g() to make it explicit, a new keyword.

Sure, in the above example it's a bit silly to want let x to be of type Void, because it's not generally useful to create a stored variable of that type. But since the last-expression-as-return-value rule allows nesting, this:

let closure = {
    if condition {
        log.info("Invoking g()")
        g()
    } else {
        // ...
    }
}

Still shows the same ambiguity problem as your original example, yet return g() wouldn't be valid syntax here (if you write return g() instead of g(), then the if is no longer an expression, and you must then write return in all the other branches, the very thing this pitch is trying to avoid).

So, if anything, your argument is one for having an explicit keyword to assign variables inside multi-line expressions (at least as an option). Otherwise you're giving up the option of being more explicit when the result of a @discardableResult informs type inference in expressions.


  1. I agree that it can be confusing at times (depending on the function name), but to me @discardableResult only means that the result can be discarded, not that it should be discarded every time the compiler has the option to do so (like in type inference in closures). So while it's nice to be able to be more explicit, I don't see it as a requirement. ↩︎

Completely agree, more options is not necessarily good for the strict systems like programming languages.

I want to note that to be fair, the change introduces new features to the language - enables multi-statement expressions. The question if is that important to alter language behaviour at a large scale? It can be addressed in less disturbing ways. Or maybe isn’t that critical to address in the end.

With this new options added to the language more to simply support a bit different style of writing I have a C++ vibe in the bad connotation - when language is just becomes more complex because of added paradigms and ways to write the code. I have never liked this side of it - you completely puzzled what’s the difference and what to use (especially if you just starting out) instead of just writing the code.

For the matter, I really appreciate Go’s approach with mandatory formatter - while this is just another extremum to handle such questions, but the benefit is that you are worry free about how write, language has decided it and you follow, no questions asked.

Adding such approach to the Swift makes no sense, as this is controversial approach after all, but limiting options to choose from to the certain level sounds reasonable. Use of keywords, especially return, in mostly imperative language, to me seems to be way below that level.

2 Likes