Pitch: Implicit Returns from Single-Expression Functions

The Never case isn't about a function or variable that returns Never, but rather something like this:

var foo: Int {
  fatalError("I'll get around to implementing this on Sunday")
}

That's something we can't break, but syntactically it looks exactly like the usual implicit return.

4 Likes

Looks good to me. I’ve always found this inconsistency a bit odd and occasionally wished I didn’t have to write return in these situations.

3 Likes

This is similar to what Perl does. The value of the last statement executed becomes the return value of the block.

I think this looks really great and I'm looking forward to have this in Swift. Since you mentioned Scala in the last paragraph there is something else they allow which I think would be a big benefit for the proposal: have the expression after an = symbol instead of curly brackets.

This would make it much more obvious that this is the value that gets returned and makes it distinguishable from the current syntax.

So I propose to change the syntax from

func functionName(parameter1: Par1, parameter2: Par2) -> Ret {
    expressionThatReturnsRet()
}

to

func functionName(parameter1: Par1, parameter2: Par2) -> Ret = expressionThatReturnsRet()

Function bodies with {} would still need the explicit return keyword. This syntax could also be forwarded to other occasions like subscripts or get/set:

var foo: Foo {
    get = _foo
    set { _foo = newValue }
}

From the top of my head I know that Scala and Kotlin are allowing this (Haskell as well kind of). What do you think?

EDIT: Just saw that @Francois_Green proposed pretty much the same in Pitch: Implicit Returns from Single-Expression Functions - #8 by Francois_Green. :+1:

3 Likes

I like the change Benjamin did here.

In the original pitch it's very hard to read what exactly is returning. When you are new to the language you have no Idea what's going to happen here. Especially if you don't have an IDE available.

2 Likes

Excuse me? Can I ask why my proposal of the exact same thing was pushed back so hard especially by @Chris_Lattner3 back then? I appreciate all the work that author has done on this proposal but he missed the historical research on why this was not pushed forward so far.

By this comment this feature was simply put onto the bookshelf for many many years until Swift evolved so much that there is not much to add except convenient sugar:

The same proposal long time ago, before an implementation was required:

To be clear with everyone, I'm not trying to flame here nor do I put a brick in front of this proposal, as I proposed it myself already a few times, but I'm a little bit speechless and as I mentioned above already I appreciate the work from the current author.


Other than that, I'm +1 on this.

9 Likes

+1 from me! Ben Cohen's tweet nails it: https://twitter.com/AirspeedSwift/status/1108902065634328581

Although I like this aesthetically, and for its consistency with other languages, there's an ambiguity hazard here because many introducer keywords in Swift are contextual, including get and set. This is already valid, albeit unlikely:

var get = 0
var foo: Void { get = 0 } // a get only computed property
6 Likes

I'd actually be -1 on this as currently pitched.

Ruby allows implicit returns everywhere and I find it to be very confusing so I always am explicit in my returns anyways (coming from C/C++, python, and swift).

I do like the suggested alternate syntax that requires expressions in {} to still use return while declaration = expression can be the "implicit return" expression. This eliminates my confusion of not seeing a return and wondering why something is being returned anyways. There is no doubt about what the = means in my mind.

1 Like

I think this looks great. The nice part is that the new contexts that this expands skippable return to always have types specified explicitly, so there's much less chance for any kind of ambiguity.

I will say that I hope we can provide good error messaging / fix-its for the case where someone adds an additional line to a function using this feature. Perhaps if the compiler runs into code that doesn't have a required return statement, it can look to see if the final statement in the function body matches the function's type and offer a fix-it.

4 Likes

Great idea! Added that diagnostic here: https://github.com/apple/swift/pull/23251/commits/cfbd96ad1361e31287b51336694e26ed9865b231

 

Now

public func hi() -> Int {
    print("entering hi")
    17
}

gets the more helpful diagnostic with a fixit

> debug-swift test.swift
test.swift:3:5: warning: integer literal is unused
    17
    ^~
test.swift:3:5: error: missing return in a function expected to return 'Int'; did you mean to return the last expression?
    17
    ^
    return
11 Likes

FWIW, I still feel exactly the same way about this, and I find that the "just omit the return" proposal to be super problematic from a clarity perspective.

The func f() -> Int = 42 variant is a significant improve on that clarity issue, but I still don't think it is "worth it" to add complexity to the language for this.

-Chris

9 Likes

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