SE-0255: Implicit Returns from Single-Expression Functions

I have a feeling that this is a feature that would be very welcomed by the FP crowd and potentially rather disliked from people accustomed to a more OOP mindset.

Personally, I'm a bit more in the former camp and I generally prefer expression-based languages (if/else not being an expression is another pet peeve...), having mostly written Ruby before (and some JS, where I always hated when I would forget the return keyword; at least, in Swift, the compiler will warn you). But I know other people dislike Ruby's implicit returns.

I would be against this feature being only adopted for e.g. computed properties, but not for general functions. IMHO, that would create an annoying inconsistency, and I would rather prefer not having the feature at all than having it in that way.

I'm also somewhat intrigued by the func foo(x: Int) = x * x syntax proposal; I think Scala does it like that?

3 Likes

You are probably right.
I am from the latter camp and I really dislike this proposal, at least for the case of functions.

I find that code readability would really suffer from the omission of return (at least in the case of quickly glancing through a piece of code).
I’ll take the sum function used in the motivation section of the proposal as an example: in its current form, I can just take a quick glance at it, reading only the beginning of each line, and I will know both its name and that it returns a result.
With the omission of return, the process is noticeably longer, as explained in the proposal itself:

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.

That sounds like a lot to do in order to reach the same conclusion that seeing the return keyword would bring me to.

Another annoyance is that not all expressions read like expressions right away. Some look more like a statement when taken in isolation. It is interesting that the proposal uses precisely an example of such an expression (reduce) for its motivation section.
There again, I find the return keyword to be useful for making things explicit and forcing what follows it to be read as an expression.

And of course, all this will be made worse when going through a mix of code using return and code that omits it.

It does not seem to me that the gain of saving 7 keystrokes is worth it.

I suppose I belong to the OOP crowd. In my personal experience, I'm mostly exposed to the omission of return with the Ruby language. My daily job is mostly spent writing Swift and a little ObjC, and sometimes I enjoy a little Ruby.

Each time I switch language, it takes a few minutes or hours until I become fluent again. It happens that I add "useless" returns in Ruby (that I remove later, usually during code review), or omit "required" returns in Swift (that I add right-away because, you know, compiler).

Based on this experience, I do not think that the presence or absence of return is part of the language identity, or any OOP/FP "affiliation". To me, it's part of the developer's muscle memory. And muscle memory is highly malleable.

On the other hand, elision of return in "single-line code chunks" is currently part of the language identity, thanks to closures, and I appreciate a lot that the proposal respects this.

2 Likes

And as someone from the former camp, my main issue with the proposal is that it doesn't go far enough. I'd love to see all returns eliminated (though I understand why that won't happen).

That process isn't really any longer; you just take a quick glance at just the first line and you know both its name and that it returns a result [well, really, you know the type that's returned, as no return type indicates a return of type ()].

I.e. for this example, I look at that function, see -> Element and know that it returns something of type Element. You don't even need to see that it's only one line to know that it must return something, and even with explicit returns, you still have to rely on the lack of errors to know that someone didn't try to return something of a different type than is indicated.

I suppose this is just another example of having soaked in FP/OOP for a long time. I see reduce and think expression, where you see it and think statement. I may very well have used reduce in the example myself as I would no more think that was a statement than 1 + 1.

So if you were to come across something like:

func boo(_ name: String) {
    return print("Howdy, \(name)")
}

Would you really read the print statement as an expression there? Would you mentally treat that code differently just because the return is in there? Because the compiler doesn't treat it any different.

The clarity, at least for me, is in the signature of the function, and not the presence or absence of the return keyword.

func foo() -> Bar { bar() }

Using a linter to encourage consistency will solve this problem in a team environment.

1 Like

Asking the compiler to help me write less words is also why I am in favor of this proposal.

Upon further reflection, this form deserves consideration as an alternative to this proposal. Has it been pitched previously?

1 Like

Could this syntax (from @Chris_Lattner3) be extended to computed properties as well?

var sum = reduce(0, +)

// instead of
var sum: Int {
    return reduce(0, +)
}

It would be very elegant, especially if we could omit the type just like we do for non-computed properties, thanks to type inference:

var count = 0
var sum = reduce(0, +)

Thinking of my own humble SQLite wrapper, this would greatly enhance the definition of record associations:

// Currently
extension Author {
    static let books = hasMany(Book.self)
    var books: QueryInterfaceRequest<Book> {
        return request(for: Author.books)
    }
}

// Future?
extension Author {
    static let books = hasMany(Book.self)
    var books = request(for: Author.books)
}

// Enjoy
let author: Author = ...
let books = try author.books.fetchAll(db) // [Book]

A very passionate -1.

No. I do not believe this proposal addresses any problems. In fact, I believe it causes harm.

This proposal assumes an implicit return is somehow "better" or more clear than an explicit one, yet it does not provide any evidence to back this. From my own experience, as someone who teaches programming for a living, unnecessary sugar makes programming languages harder to use and understand. In particular, I would like to speak out for my many, many students who fall into one or more of the following categories:

  • chose to study computer science because it seemed like an interesting profession with lots of job opportunities, but don't consider themselves "a geek",
  • have some form of autism (as a trait, not a disorder),
  • have a very mathematical, analytical mind.

These students tend to appreciate explicitness. Whenever they encounter sugar, they tend to remove the sugar in their mind so they can better understand what's going on. This means unnecessary sugar increases the cognitive load of reading code, which is the opposite of adding clarity.

Just to give you an example, here's the cognitive load required to process the motivating example in the proposal:

func sum() -> Element {
    reduce(0, +)
}
  1. "OK. So this function calls some other function named reduce."
  2. (student recalls the design guidelines) "This reduce function is named using an imperative verb, so it does something and has side effects." (student now assumes sum simply calls reduce)
  3. "Oh wait, there's a return type." (easy to miss as it's and the end of the line)
  4. "So what does this function return?"
  5. "Oh right, there's this exception where return can be omitted if it's only a single line of code. This must be one of those cases."
  6. (student looks up reduce) "Ah, reduce does return something instead of having side effects. Weird, that must be an exception as well."

In this example, the return keyword plays an important role in helping the student understand what's going on. Removing it makes it much harder to understand.

So far, the core team has done a really good job at carefully selecting which sugar to include and which not to, but I don't feel like this proposal meets the standards the language has set so far. I understand where the proposal is coming from, and that it looks like an obvious improvement to many, but I don't feel like it solves a real problem and it has the potential to reduce clarity instead of improving it.

In general, I would also like to ask the community and the core team to take the utmost care when it comes to accepting new sugar. I don't see many proposals backed by studies that prove the proposed features do indeed improve the language for the general public (of which this forum is probably not a good representation). Without proper evidence, I don't think marginal gains are enough for a proposal to be accepted.

6 Likes

This doesn’t work. The top var in your example is already valid syntax and is a stored property. I discussed this direction upthread. It would require new syntax. I used := as a strawman.

I generally don’t think this direction is a good idea unless we are willing to consider return type inference when the := shorthand is used to declare a function. I can’t think of any good reason why we would allow return type inference for a computed property but not for a function.

If we aren’t going to support return type inference (we probably aren’t) then I think this proposal is the best and most consistent direction. Using braces only requires one additional character over the = syntax and zero additional characters over the := syntax. It is also consistent with Swift’s current design that all code bodies appear inside braces (with the single exception of direct initialization of stored properties).

This syntax could work for functions, but not for properties which already use the = operator:

// Already looks for an available `reduce` method in the current scope
var sum = reduce(0, +)

A possible solution would be to use the := operator, which is already used in several languages as a definition operator, and is not yet used in Swift:

func sum() -> Int := reduce(0, +)
var sum := reduce(0, +)

I also think that this syntax could address several concerns of @svanimpe above. Do you think it would help students understand that reduce returns a value despite its imperative form?

• What is your evaluation of the proposal?

+1

Implicit returns from single-expression functions and properties seems like a natural extension of Swift, and matches implicit returns from single-expression closures.

• Is the problem being addressed significant enough to warrant a change to Swift?

Yes

• Does this proposal fit well with the feel and direction of Swift?

Yes. The parallel with single-expression closures seems elegant and Swifty to me.

• How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Read the proposal and followed the discussion.

Jeremy

Hmm. This might be too much type omission.

The type inference system depends on having type information at the point where functions and properties are declared. Routinely omitting that information would make the system work much harder and slower, and lead to more frequent unsolvable cases.

Also, in practice, this might be harder to read at a glance if not used judiciously.

2 Likes

So, now, I am way off topic. Apologies to everyone. A couple of quick thoughts, and then I will refrain.

  • Consistent Syntax: I think the operator (if any) and right-hand sides for functions and computed properties need to use the same syntax. This suggestion accomplishes that objective.

  • Syntax for Type of Computed Property: I'm not sure of the explicit type syntax for the computed property. Would it be var sum: Int := reduce(0, +)?

  • Preference: I don't know whether users would prefer this approach over that championed by the proposal at hand. Do we need data? How does one obtain such data?

  • Consistency of Braces: Omitting the braces in these instances isn't great. It feels like the language would be heading down a path of having a mish-mosh of syntax. Would it be better to use the plain = operator, and keep the braces on the right side? Such as:

func sum() -> Int = { reduce(0, +) }
var sum: Int = { reduce(0, +) }    

Do you disagree with the use of implicit returns in closures, and do you think that's comparable to this?

Good point.

I’m not convinced that adding = would serve any meaningful purpose over the proposed syntax.

This:

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

vs:

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

1 Like

My apologies. It was just an exploration of Chris Lattner's suggestion for an alternative syntax. And clearly this is not what is proposed in SE-0255. We can just leave that here.

I think closures are a different story.

collection.filter { $0.age >= 18 }.map { $0.name }

With closures, you tend to think only about the expression. In the example above, you're filtering based on a condition and mapping to a value. You're not really thinking about declaring a function that returns a value; that's just an implementation detail.

My experiences with closures are that:

  • students with prior programming experience tend to struggle with the syntax in general because they've only used arrow syntax so far (e.g. in Java or C#).
  • students learning Swift as their first language have less issues with the syntax because they have no preconceptions as to what closures should look like. They tend to struggle mostly with trailing closures. I've seen a lot of these students avoid trailing closures at first and only adopt them when they have a better understand of closures in general.
2 Likes

Proposal Accepted

The review demonstrated clear support for this syntax for property and subscript getters, where the need to return simple single expressions is common.

On eliding the return from functions, the review feedback was more divided. While there was support from some reviewers, others expressed the view that the feature should be restricted to just getters. However, on reviewing the feedback, the core team does not see a strong case was made against applying it to functions as well, when weighed against the benefits of a single uniform rule.

Some reviewers floated the idea of a different syntax for function declaration, using = or => , as seen in other languages. The core team doesn't feel this syntax handles properties or subscripts well, and does not consider sugaring function declarations specifically with a separate syntax a useful change.

The core team also acknowledges that accepting this feature may encourage follow-on proposals that generalize it further, such as converting if/ switch statements into expressions, or allowing elision in multi-statement bodies. Accepting this proposal doesn't necessarily endorse or rule out those future directions.

Thank you to everyone who participated in this review!

Ben Cohen
Review Manager

23 Likes