SE-0255: Implicit Returns from Single-Expression Functions

Really, both of these forms are motivated by legitimate reasoning, so neither of them is arbitrary. But I disagree that my hypothetical rule is more arbitrary. The refactoring required to transform

numbers.map { $0*2 }

into

let double = { $0*2 }
numbers.map(double)

is of a conceptually different sort than going from

let double = { $0*2 }

to

func double(_ x: Double) -> Double { return x * 2 }

In any case, the hypothetical adjustment I presented is fairly off-topic for extended discussion on the proposal review thread.

Regardless of how I think it should be, though, the current state of affairs doesn't strike me as wildly arbitrary either. The step from closure to func is essentially a transformation of "let me be very explicit about several things that were implicit before," which applies to argument names, argument types, return types, and actually specifying return. To me, there's nothing wrong with that.

2 Likes

+1 for consistency

Consistency isn't even the most convincing argument to me. I simply think it's intuitive but non-essential shorthand that will always be desired by users writing compact one-line functions and getters.

1 Like

Well, at the point where one chooses to use func, one is required also to use return. In other words, it isnā€™t an arbitrary point along the continuum, divorced from all other syntactic changes, that the requirement for return kicks in. It isnā€™t any more ā€˜consistentā€™ to make it optional there than to articulate a simple rule: where these is func, there must be return.

4 Likes

The rule stated that way is actually: where there is func or more than a single expression there must be return. That is clearly a more complex rule than: where there is more than a single expression there must be return. The reduction in complexity of the rule makes the language simpler overall. The difference is admittedly marginal, but it would be quite meaningful when measured by the experience of Swift programmers (many of us) who upon learning the closure shorthand immediately thought it would make sense to allow implicit return anywhere there is a single expression (and therefore thought the limitation was somewhat arbitrary).

3 Likes

No, the statement is correct: where this is func, there must be return. This does not imply that where there is return, there must be func.

You missed the point of my reply, which is that it is not at an arbitrary point in the continuum outlined by Ben that return is required, but precisely where there is func.

2 Likes

I didnā€™t say that it does. I just pointed out the complete current rule for when return must be present.

I understand that is your point. I was making a further point that while it may not be completely arbitrary in that sense it does feel rather arbitrary (at least to many of us) in the context of the complete rule for where return is currently required.

That is when. It is not why. It is the lack of a why that is arbitrary. The rule for when the change occurs can be clear and well-defined, but still arbitrary.

7 Likes

You could say the same thing about how when there's a func there's also a fully-written explicit type signature.

1 Like

FWIW, as I pointed out upthread if we consider the assignment-like sugar instead of the currently proposed sugar then this also starts to feel arbitrary. Why can the type of a stored property be inferred but not the type of a get-only computed property or subscript or the return type of a single-expression function when these constructs are limited to single expression bodies?

The primary principle behind our inference boundaries as I understand it is that we don't infer types across statements. That principle is not relevant for single-expression bodies.

Type inference for non-local declarations (those outside of function bodies) can have a significant impact on compilation time, so I'd rather not mess with that. I'm also on the ideological side that defining your interfaces clearly is a good thing, even when you're just talking about the interfaces within your own module, but that's a fuzzier argument.

(Rust is also in the "we'll infer a lot of types for you, but not in your declarations, except lifetimes" camp. Haskell is on the other side.)

1 Like

I can think of a few good reasons for this that I would happily reel off if someone suggested functions types could be inferred (functions are contracts, you'd need to infer the types from the body multiple times while building which is an argument against inferred stored property types, etc).

What I am looking for is similar reasons for the return elision being a no-go for functions in particular.

To put my review manager hat back on: reviews on evolution are not ballots. It is not especially useful to say you are for or against something, that you like or dislike something, other than in simple cases where all comments are in favor. It is the rationale that is important. The core team reviews the discussion looking for reasons the proposal should be accepted, rejected, or modified.

So what is needed here if the core team is to (for example) decide that return elision should be accepted for properties but not for functions is a clear reason why it would be harmful to allow it for functions in particular, while being good to allow it for properties.

3 Likes

Iā€™m not arguing for or against this, just trying to point out how things look from the programmerā€™s perspective. There may well be compilation time issues that would exist for single-expression bodies that donā€™t exist for stored properties. It isnā€™t clear to me why that would be the case but Iā€™m definitely not an expert in this area!

Unless the return type is Void. Swift allows elision in many scenarios, when it makes sense. And that's without getting into type inference.

Return elision in functions only makes sense to me if we have full-blown statements-as-expressions like Rust. Functions are named callable chunks of code and there's nothing special about the last statement in a function that warrants it being given special behavior.

Also, unlike property getters and closures, having a Void return type is quite common for a function. If I look at a function body without its type signature, I can tell what the function's return type is... unless we have return elision. Then I can't. Which could also lead to more confusing error messages if the function's body doesn't match its return type.

There's also the stylistic angle to consider. Single-expression closures are frequently written on a single line, such as { $0 + $1 }. That's the core argument in favor of return elision. Property getters are also frequently written on a single line, such as get { return label.font }. This feels very much like a closure, and even looks like I'm passing a closure to a function/keyword named get. But functions are typically never written on a single line. While you certainly could write func foo(x: Int) -> Int { return x + 1 } without any line breaks, stylistically speaking that would be quite unusual (anecdotally, I've never worked in a codebase that didn't put a new line after the opening brace regardless of function brevity). Return elision for functions really only makes sense if we expect people to not put a newline after the brace, and we don't.

7 Likes

Oh I also forgot to mention, single-line closures and property getters are extremely common. Single-line functions are fairly rare, so having return elision show up there is likely to be more confusing than helpful.

1 Like

Whoa, hang on. No one is proposing last-statement return elision.

We already don't have that:

func computeImportantConstantForGraphicsStuff(ā€¦) -> ???? {
  guard self.isShowingTheExtraStuff else {
    return 0 // ā€¦actually a CGFloat
  }
  // ā€¦
}

func fetchAllItems(ā€¦) -> ???? {
  guard self.isConnected else {
    return [] // Array of what?
  }
  // ā€¦
}

func performCachedOperation(ā€¦) -> ???? {
  guard !self.hasDoneTheCachedThing else {
    return .okay // A status of some kind, but which one?
  }
  // ā€¦
}
1 Like

True, but I don't consider the following two functions to be different in any meaningful sense:

func foo() -> Int {
    let x = 5
    return x
}
func bar() -> Int {
    return 5
}

so why should I be able to elide the return in the second but not the first?

Of course, closures also have the single-expression rule, and to be honest, I'd be in favor of allowing returns to be elided in multi-line closures as long as the closure's type signature can still be unambiguously determined (either written explicitly, or inferrable from usage without looking at the closure body), but I'm not pushing for that because it would likely lead to more confusing errors down the road ("why can I elide return in this closure but not that one?").

True, but you can at least determine the shape of what's being returned in each of those cases: A number (or at least, expressible by numeric literal), an array (or expressible by array literal), and a type that has a static member .okay. Whereas with return elision, if you don't look at the function's type signature you can't even tell whether the function is returning a value at all.

Et voilĆ : I think we've discovered the principle at work here:

It's not so much the stylistic significance of how most users use one line versus multiple lines, but that fundamentally anything spelled with func is always treated as a collection of statements.

Short closures are given a privileged syntax such that they can be spelled like an expression surrounded by braces for reasons already given; this naturally lends itself to a continuum where one extreme is actually the single-expression autoclosure, which is literally spelled as an expression.

Getters for computed properties are thought of similarly by many users (as evidenced here), perhaps because the RHS of a non-computed property literally is an expression.

But users simply don't treat functions in that way. In part, it is because the ceremony of writing out a full declaration naturally pushes users to set the contents apart. I would posit, though, in part it is because there is no close counterpart to functions where you have a function declaration followed by an actual expression.

If we had full-blown statements-as-expressions, this would not be an issue.

2 Likes

I do this.

I have no idea why this is a consideration at all (I've never heard of ā€œguessing the return type of a function body without looking at the signatureā€ being a common activity, parlour game, or Swift design goal) but this isn't generally true either. The return value can be, and often is, the return value of another function.

5 Likes