Pitch: Implicit Returns from Single-Expression Functions

Swift provides a pleasant shorthand for short closures: if a closure contains just a single expression, that expression is implicitly returned--the return keyword can be omitted. We should provide this shorthand for functions as well.

Motivation

Consider the following implementation of the popular Collection.sum() extension:

extension Sequence where Element == Int {

    func sum() -> Element {
        return reduce(0, +)
    }

}

The implementation is extremely brief, weighing in at 19 characters. Of those 19, however, 7 are consumed by the return keyword.

Proposed solution

Here's how that same property would look if the single-expression in the body is implicitly returned:

func sum() -> Element {
    reduce(0, +)
}

The expression which is being returned is front and center, not tucked away behind the return keyword.

For readers previously exposed to single-expression closure syntax, this will feel completely familiar.

Even to readers without such exposure, though, the meaning will be clear: When reading the implementation--after the var keyword and name--you first encounter the return type and then the single expression within the body. Since you've just read the return type, can see that there's only one expression in the body, and are told by the compiler that the code is legal, you are forced to conclude that the expression must indeed be returned.

In fact, exposure to functions--whose types are always stated--without an explicit return keyword will help prepare new Swift users to understand code like

let ages = persons.map { $0.name }

Their prior exposure to functions which implicitly return the single expression in their bodies will lead them to conclude that the closure being passed to map is returning the expression $0.name and that the return type of the closure is String (the type of name, here).

Detailed design

Interpret the bodies of function-like entities which consist of a single expression as returning that expression--unless the entity's return type is Void or the single expression's type is uninhabited.

Function-Like Entities

The following are the function-like entities eligible to implicitly return the single expression in their bodies:

  • Functions. For example:
func add(lhs: Int, rhs: Int) -> Int { lhs + rhs }
  • Property accessors.

With an implicit getter:

var location: Location { .init(latitude: lat, longitude: long) }

With an explicit getter and setter:

var location: Location {
    get {
        .init(latitude: lat, longitude: long)
    }
    set {
        self.lat = newValue.latitude
        self.long = newValue.longitude
    }
}

Since only the get accessor may return a value, implicit returns from single-expression accessors will only affect them.

  • Subscript accessors.

With an implicit getter:

struct Echo<T> {
    subscript(_ value: T) -> T { value }
}

With an explicit getter and setter:

struct GuaranteedDictionary<Key : Hashable, Value> {
    var storage: [Key : Value]
    var fallback: Value
    subscript(key: Key) -> Value {
        get {
            storage[key] ?? fallback
        }
        set {
            storage[key] = newValue
        }
    }
}

As with property accessors, since only the get accessor may return a value, implicit returns only affect them.

  • Initializers.
class Derived : Base {
    required init?() { nil }
}

The only legal return from an initializer is nil, and that only in the context of a failable initializer. As a result, that is the only place where an implicit return from an initializer can occur.

Exceptions

When a function-like entity's body consists of a single expression, there are two cases where no implicit return will be inserted:

  • Void return. In the following code
func foo() {
    logAndReturn("foo was called")
}

@discardableResult
func logAndReturn(_ string: String) -> String { ... }

adding an implicit return to foo would result in a type error, namely, unexpected non-void return in a void function. It is reasonable to be able to call a function (here, logAndReturn) which returns a value as the only operation performed by another function (here foo) which does not return a value. Moreover, foo as written is legal code, so we want to avoid treating this as a type error since doing so would result in source breakage.

  • Uninhabited expressions. In the following code
func vendAnInteger() -> Int {
    fatalError()
}

adding an implicit return would result in the analogous type error (cannot convert return expression of type 'Never' to return type 'Int'). Functions which return values but whose implementations consist solely of a single call to a Never returning function are an established practice in Swift--they allow users to put off defining their functions until they are ready to (or forever). With implicit returns, this function's implementation will have the same meaning as it has today: The code will compile. No implicit return will be inserted. And at runtime the call to fatalError() will never return. Source compatibility will be preserved.

There is one exception, as described in the section below:

Source compatibility

For the most part, the change is additive, making legal code that is currently illegal. It does, however, break source compatibility in one case.

In current Swift, when the following code is compiled

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

func callBad() -> Int { bad() }

the call to bad() in callBad() resolves to the second overload of that name (whose signature is () -> Never). With implicit return, the call will instead resolve to the first overload.

The large downside of breaking source-compatibility is mitigated by the fact that overload sets of which only one member returns Never are very rare: Extensive source compatibility tests have been run against this change without issue.

Effect on ABI stability

None. Implementation is only in the parser and type checker.

Effect on API resilience

None.

Alternatives considered

  • Maintain source compatibility.

Maintaining source compatibility entails teaching the overload resolution system a special case for single-expression functions. It is possible to do this but would require complicating the type checker. Far worse it would complicate the language model:

If source compatibility were maintained, the following two functions

func callBad_1() -> Int { bad() }


func callBad_2() -> Int { return bad() }

would have different behaviors: callBad_1 would trap and callBad_2 would return an Int.

In a Swift with implicit return for single-expression functions, the mental model for users should be that a return can be omitted in certain cases and that it doesn't matter whether one is included or not. Preserving source-compatibility in this case would break that mental model.

 

  • Permit implicit return for a subset of declarations.

This document proposes allowing return to be omitted from the following declarations:

  • functions
  • properties
  • subscripts
  • initializers

An alternative would be to allow that omission in only a subset of these.

Concretely, several reasons were given for allowing it in only get-only computed properties:

(1) Unlike functions, get-only properties already have one shorthand, the omission of get. By analogy to the situation with closures, that indicates that they are eligible for the further shorthand of omitting return.

Response: This argument applies equally to subscripts which support the same shorthand as properties. If the reason to permit the return to be omitted from properties is that get can already be omitted, then that reason leads also to permitting return to be omitted from get-only subscripts.

The differences between get-only subscripts and functions are already few and may be getting fewer ( Pitch: Static and class subscripts , [Draft] Throwing Properties and Subscripts ). It would amount to a language inconsistency to allow get-only subscripts but not functions to omit return.

(2) Unlike functions, get-only properties always have a return type.

Response: In standard usage, it is much more common to encounter functions which return Void than properties. However, while that usage is far more common, the following is still part of the language:

var doWork: Void {
    work()
}

 

  • Making uninhabited types be bottom types.

As currently implemented, an implicit conversion from an uninhabited type to any arbitrary type is permitted only if the uninhabited type is the type of the expression in a single-argument function and the arbitrary type is the the result type of that function. If every uninhabited type were a subtype of every type, this implicit conversion could be applied across the board without special casing for the single-argument return scenario.

While such a feature can be implemented (see the uninhabited-upcast branch), it doesn't maintain source compatibility or otherwise relate to this feature except in terms of the compiler's implementation.

 

  • Use braceless syntax for single-expression functions.

Some other languages such as Scala and Kotlin allow single-expression functions to be declared without braces. In Kotlin, this looks like

fun squareOf(x: Int): Int = x * x

and in Scala, it looks almost identical (the only difference being the use of def instead of fun).

def squareOf(x: Int): Int = x * x

Those languages' syntax suggests a similar approach be taken in Swift:

func square(of x: Int) -> Int = x * x

For functions, this might be fine. For Swift to be self-consistent, a somewhat similar would be needed for properties and subscripts.

var value: Int {
    get = _storedValue
    set { _storedValue = newValue }
}

Unfortunately, this begins moving into ambiguous territory:

var get: ()
var value: Void {
    get = ()
}

In this example, it's unclear whether the braces of value either (1) enclose an explicit getter for value whose implementation is a single-expression function returning () or alternatively (2) enclose the body of an implicit getter whose implementation sets the get property to ().

 

  • Allow implicit return of the last expression even from bodies which consist of more than a single expression.

Rust, for example, permits this. Given functions foo, bar, and baz, all which return integers, the following is a legal function in Rust:

fn call() -> i64 {
    foo();
    bar();
    baz()
}

While this could be permitted in Swift, doing so would lead to asymmetry in code resulting from the fact that Swift is not expression-oriented as Rust is. Consider a function with some basic branching:

func evenOrOdd(_ int: Int) -> EvenOrOdd {
    if int % 2 == 0 {
        return .even
    }
    .odd
}

Here .even is returned for even Ints and .odd for odd. Notice that only one of the two returns from the function uses the return keyword! The same unpleasant function could be written in Rust:

fn even_or_odd(i: i64) -> EvenOrOdd {
    if i % 2 == 0 { 
        return EvenOrOdd::Even 
    }
    EvenOrOdd::Odd
}

In Rust, though, the asymmetry could be resolved by implicitly returning the entire if expression:

fn even_or_odd(i: i64) -> EvenOrOdd {
    if i % 2 == 0 {
        EvenOrOdd::Even
    } else {
        EvenOrOdd::Odd
    }
}

That option is not open to us in Swift because conditionals are statements, not expressions in Swift. Changing Swift into an expression-oriented language would be a radical transformation to the language and is beyond the scope of this change.

 

  • Allow the return type to be omitted from the function declarations.

Scala, for example, permits this. In the following code

def squareOf(x: Int) = x * x

the compiler infers that the type of squareOf is (Int) -> Int.

Haskell takes this further, permitting functions to be written without either explicit inputs or outputs:

{-# LANGUAGE PartialTypeSignatures #-}
fac :: _
fac 0 = 1
fac n = n * fac (n - 1)

While these features are arguably nice, they greatly increase the complexity of type inference, and are out of scope for this change.

34 Likes

Sounds good. I think Groovy also allows this kind of implicit return.

I would probably be in favor of supporting this in any function, not just single-expression functions but it seems that Swift closures also only support implicit returns in simple closures. So this would match that.

For cases where closure is specifically expected to return clearly defined value (or Void), this pitch might be fine, but for cases where return value is more ambiguous (e.g either void or value), there might be situations where developer accidentally uses non-returning variant of function when developer meant to return value. In those ambiguous situations it’s better to always include ”return” when value returning function is used, so that code is more easily understood

EDIT: My point above is slightly misguided, as closures already allow the shorthand syntax, so this is only about functions.

However, I see closures overall as shorthand (also including the special parameter syntax), and functions as the more fully formed syntax. I think it's good that they are separate, and don't see need for functions getting a shorthand syntax like this.

Could you provide a concrete example illustrating the scenario you're concerned about?

This isn’t a bad proposal, but I find myself wishing for implicit return more often on single line computed variables than I do on actual functions.

7 Likes

Yeah, that's my common use case too, and the one I'm most happy about in this proposal. Applying it to regular functions is more of a "sure, I guess it's good to be consistent" thing for me.

8 Likes

Would it make sense to explicitly exclude types Void and Never from this proposal? If feasible, that would simply avoid the edge cases where the intention might be unclear (and would be 100% source compatible).

Limited to only single expression functions, maybe this wouldn't be too confusing. That said, I like the last alternative best, but with a further limitation of just local functions only (does limiting the scope help with type inference?) and with a fat arrow. That would give programmers a real one-liner:

func multiplyByTwoAndThree(a: Int) -> (Int, Int) {
  func multiply(b: Int) => a * b
  return (multiply(b: 2), multiply(b: 3))
}
1 Like

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. :+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