Pitch: Implicit Returns from Single-Expression Functions

Nice! That looks like a great improvement independent of this new language feature. :+1:

How about just for computed properties?

1 Like

Given the syntactic similarity between computed properties and simple closures, I am not sure how one would justify an arbitrary difference such as this one.

2 Likes

A more limited change such as this would be easier to justify, IMO. Essentially the only time I have reached for this feature and been disappointed has been in scenarios when Iā€™m implementing simple getters.

Computed properties already have a shorthand to omit ā€œgetā€. I think a rule to allow one-line getters also to omit ā€œreturnā€ seems extremely reasonable.

We would also sidestep any type inference issues (and the associated backward compatibility problems outlined in this pitch) as computed properties must state the type, and for the same reason Iā€™d argue that clarity doesnā€™t suffer.

7 Likes

To clarify, the backwards compatibility issue is not tied to the func case.

In Swift today, the following is legal code:

func bad(value: Int = 0) -> Int { return 0 }
func bad() -> Never { return fatalError() }

var getBad: Int { bad() }

and the call to bad in getBad resolves to the Never-returning overload.

With the pitched change, the above would remain legal code but get the call to bad in getBad would resolve to the Int returning overload.

As described in the pitch, the rationale here is that the alternative complicates the mental model: an implicit return is supposed to be the same as an explicit return; an explicit return can be omitted whenever a function's (property's, subscript's, or initializer's) body consists only of a return statement with a single expression, and omitting it should have no effect. Resolving on the basis of the presence/absence of the return keyword complicates matters.

I had my thinking reversed. Closures already allow an implicit return keyword, and functions don't. I agree with you that it makes perfect sense to extend this to getters in particular. They look more like closures than functions, and the inconsistency in allowed syntax already exists.

I do have to admit, that coming from years and years Scala development, this is one of the things I miss.

I thought that adding some context how and why it works well there might give some more perspective here, and actually also why that exact same mechanism may not be all that suitable for Swift (unless bigger changes were made), hope this helps anyone who's wondering about the "but X does it" points.

One thing to keep in mind when pointing out Scala as an example here is that this feature is only really powerful and useful because of its interplay with with flow control statements like if / switch being expressions. This then allows always writing code with the assumption "last thing is returned:"

// def example: Int = 
def example =  // or inferred `: Int`
  if (conf) 2 else 3

// or even
val example = 2
val example = if (cond) 2 else 3

One has to keep in mind the other implications of the language's overall style when discussing this as well I feel. E.g. in Scala it is highly unusual to write "early returns", as the style leans heavily to exploit the "read until you hit last branch's last statement, and this is the returned value.

In Swift we often do early returns, for example because the guard statements encourage the style of guard ... else { return ... }, where I agree the return makes it much more clear to readers that "aha, we're returning early here!", even if they have no idea about guards. So it seems to me that to remain true to this style, and extend the implicit return to only a few specific things: single expression closures, getters, functions (?).

Overall sounds like a good idea and I hope it can be polished up and land :slight_smile:

True, however the following code is also legal, and resolves to the (Int)->Int overload:

var getBad2: ()->Int = { bad() }

I think it is seriously problematic that the computed property acts differently than the closure here. We have a high bar for source-breaking changes, requiring that the existing behavior is ā€œactively harmfulā€, and I believe this situation rises above that bar.

A reasonable developer could quite easily write the line you have for getBad, forgetting the return and expecting it to work just like a closure and call the Int-returning overload, only to discover laterā€”hopefully through testing but possibly not until after deploying to clientsā€”that it actually crashes the application at run-time.

This is bad, and should be fixed.

2 Likes

+0.5

IMO this should already exist for gets (single-expression or otherwise) since they're explicitly returning something by definition.

I do question whether it's really necessary in regular functions/methods. It's compromising clarity for brevity, and I'm sure would result in instances where people (e.g. myself) would limit a function to one expression that should be split into multiple lines so as to not add a return.

Also based on my experience with Kotlin,
fun squareOf(x: Int): Int = x * x
tends to be more confusing than helpful. It merges the "code shape" of properties and functions in a way that's confusing to new users, or people who switch between languages often (again, myself :innocent:)

Especially since Swift can reference functions via thing.squareOf, IMO function declarations should remain syntactically distinct from property declarations.

6 Likes

+1

Making closures, computed properties and functions behave similarly wrt return rules seems well worth it to me.

1 Like

I like the idea especially since we can implicitly return from closures already.

class Greeting {
    lazy var message: String = {
        "Hello, world!"
    }()
}

Or something thatā€™s more common in our code base:

let integers = [1, 2, 3, 4, 5]
let strings = integers.map { "\($0)" }

+1 from me.

+1
I wish we went further and made control flow expressions but I'll take this.

3 Likes

Iā€™d be in favor of this change. I donā€™t think having to spell out return is a huge issue, but it adds visual noise to one-line function/property declarations that has always bothered me slightly.

Such declarations tend to occur in large quantities in thin adapter types that forward most of their implementations to a base value; Iā€™m in favor of anything that makes those easier to read.

Additionally, closures have trained me not to spell out the return. The compilerā€™s rejection of this in other contexts always startles me when it happens, which is surprisingly often.

I would be surprised if the source compatibility issue would be exhibited by any real codebase. Nevinā€™s example of a case where the current behavior causes ā€œactive harmā€ seems equally theoretical, although it does illustrate the pitfalls of this syntactic inconsistency.

5 Likes

I personally am -1 for implicit returns from function declarations, but +1 for computed property declarations. I don't really feel like parity between function and closure syntax makes sense for its own sake, and there are already decisions made which make clarity the goal of function declarations, and terseness the goal of closure expressions (e.g. closures participate in type inference and allow for unnamed parameters).

OTOH, computed properties often express simple transforms of stored property elements, similar to how closures often represent simple transforms of their parameters. In this context, I think having the implicit return is actually more expressive:

var isEnabled: Bool { self.foo != nil }
var isEnabled: Bool { return self.foo != nil }

The first, IMO, more clearly communicates that the value of isEnabled is synonymous with foo being present, while the second is marginally more opaque. Computed properties in general have a lot of decisions made for the sake of terseness, so this seems like a natural extension to that feature.

8 Likes

I mentioned earlier how I was -1 in general for this proposal, but as it's been pointed out that closures already permit this behavior I would be in favor of unifying the getter closure syntax to match the other behavior of single line map, filter, reduce, etc closures.

Still a -1 on this being permitted in any old function though unless it had a separate and unique syntax. Though I may consider that kind of change to be a separate proposal from just unifying the computed variable get closure syntax to match that of other simple closures.

Big +1.

I'm a big fan of implicit returns in general, and feel right at home with Erlang/Elixir's model where the last statement is simply the return.

The cherry on top here would be implicit returns in guard else clauses, or a world in which this works:

guard someCondition else { XCTFail() }

We're not ever going to be able to do the guard one in general because it's not obvious that return is the right thing to do. (This isn't quite the same as what's written there but it's close.)

10 Likes

Sold! :grin: But this seems like a careful and well thought out pitch as well.

I have often wished for this feature, especially for getters (as many have already mentioned).

The case for getters seems pretty strong. There's essentially no argument for implicit returns in the context of closures that does not also apply to getters.

Implicit returns for functions seems less compelling, but once you get beyond a single exceptional case (closures), the widespread inconsistency would, I think, start to seem really jarring. So all things considered, I'm for just biting the bullet and making implicit returns a general rule of Swift behavior.

A couple of people have mentioned implicit returns in Ruby. They are wonderful. They facilitate the use of microfunctions and often lead to really elegant code. Unfortunately, much of their value derives from the fact that Ruby doesn't have statements, just expressions.

1 Like

+1 for me

I'm not a fan of languages that implicitly consider the last line in a scoped context to be the "return" of that scope, and I like guard and returning early in general. But in case of a single expression is actually great. A lot of times I literally only write a single expression in a scope, so the return there is definitely redundant: we don't need it in single expression closures, and there's no reason to not enjoy the same convenience for single expression functions and computed properties.

2 Likes

Okay +1 but use lambda expression syntax. =>

I donā€™t really buy the original argument that -> and => are visually too close. Tell that to commas and periods? if we can happily distinguish between colons semicolons periods and commas we should be able to distinguish between - and =.