SE-0255: Implicit Returns from Single-Expression Functions

(Matt Rips) #83

I too do this.

Removing the return creates no ambiguity. That holds true even if the single expression is another function, because the return type is expressly declared immediately before it.

func fooOne(x: Int) -> () { view.reset(toPosition: x) }  
// the reset(toPosition:) function's return type must match the Void 
// return type of fooOne(x:); thus, it is Void, and there is no implicit 
// return

func fooTwo(x: Int) -> Result { view.attemptMove(toPosition: x) }
// the attemptMove(toPosition:) function's return type must match 
// the Result return type of fooTwo(x:); thus, it is of return type 
// Result, and there is an implicit return

IMO, these one-liners are well-formatted, easy to read, and appropriate in length.

Most of the ceremony surrounding function declarations (I surmise) is about type-safety and creating an explicit base from which to perform type-inference elsewhere. The function name, the label names, the argument names, the argument types, and the return type all serve the type system.

By contrast, the return statement only tangentially relates to the type system, and does so merely by pointing the parser and type checker to one or more expressions that are intended to match the return type of the function. When there is only a single expression present, if the function has a non-Void return type, then that single expression must be the thing that is being returned and must match the return type. There is no ambiguity, and no loss of explicit type information.

8 Likes
#84

Most of the discussion about excluding functions from this proposal talks about instance methods, but I think nested functions should also be considered. If you want to extract some code into a local helper in a complex function you could write a closure:

func complexFunction<T: FloatingPoint>(…) {
  let score: ([T], [T]) -> T = { zip($0, $1).map(*).reduce(0, +) }
  …  
  let s = score([1, 2, 3], [4, 5, 6]))
  …
}

but you might decide you want to label the arguments and write a nested function instead

func complexFunction<T: FloatingPoint>(…) {
  func score(values: [T], weights: [T]) -> T { zip(values, weights).map(*).reduce(0, +) }
  …
  let s = score(values: [1, 2, 3], weights: [4, 5, 6]))
  …
}
// Error: Missing return in a function expected to return 'T'

Perhaps in future you will be able to label closure arguments (an as-yet-unrealised followup to SE-011) which would either make this less persuasive (“just use a closure instead”) or more persuasive (“there are now even fewer differences between closures and functions but you still can't elide the return from functions”) depending on your perspective. Similar is true of other oft-rumoured features, like allowing closure properties to satisfy method requirements on protocols.

6 Likes
(Spencer Kohan) #85

What do you see as the benefit of the fat-arrow syntax for closures? It seems to me that the current closure syntax has a lot of benefit in the case of trailing-closure syntax.

(Preston Sumner) #86

I think this proposal would encourage that style of formatting. Anecdotally, it's something I've seen before in Swift and Objective-C code, and I do it myself. It stands out as a useful signal that says at a glance, "This method is very simply expressed."

1 Like
(Francois Green) #87

A reluctant +1. I'm in favor of this proposal although I'll never be truly happy with it: I think a separate concise function syntax is a better way to go. That said, if for the sake of consistency alone, lifting the return requirement should be applied to functions along with properties and the like.

(Adrian Kashivskyy) #88

What is your evaluation of the proposal?

+1 for implicit returns in read-only computed properties, -1 for the rest. Overall -1.

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

No, I don't think so. Introducing yet another "gotcha" just so we don't have to type return sometimes is not a significant enough of a problem.

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

No. It fits Ruby more than Swift.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Yes, Ruby has that feature and I hate it with all my guts. In Ruby, you can't distinguish between functions that were designed to return a value and functions that are void but happen to return a value as a result of an expression. I know this wouldn't be possible in Swift because of strictly specified return type, but still, it makes me wanna cry when I think about that.

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

I read the proposal.

2 Likes
(Jon Shier) #89
  • What is your evaluation of the proposal?

+1 for computed properties. I hit this issue almost every time I use them. Everything else is -1, with an overall -1 for everything together. Function declarations already require more formality, so a single return doesn't save much.

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

Overall I'm not sure, since I don't think the issue is very prevalent for functions, but it would be a nice change for computed properties.

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

Again, the compute property part feels natural, but it doesn't seem like Swift is tending towards informality in general.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Kotlin, though I haven't used it heavily. I actually find it awkward to switch between the informal syntax when I have a single line function and the more formal syntax when I need to make it multiline, so I don't really like how it feels. But I haven't used it much.

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

Read the thread and the proposal.

3 Likes
(Krzysztof Siejkowski) #90
  • What is your evaluation of the proposal?

+1.

I've always experienced the current solution that makes me write explicit return in one case and not the other puzzling and inconsistent.

Also, I really like the possibility of using the shorthand for functions that are, well, short. I think that the code is easiest to parse when there's some correspondence between the concepts and their textual representation. If the function is short and readable enough to be a single expression, I think it's best to express it in a shortest way possible, which means omitting the return keyword.

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

Not sure about it. It's not something that I've lost any sleep over during last 4 years. On the other hand, it's just a syntactic sugar that simplifies the mental model a little bit for me.

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

Yeah, I feel so. It's ergonomics.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I've used and liked it in Kotlin, C# and Scala. It's been always a nice addition for me, a little bit of syntax sugar that just makes things look nicer.

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

I've read the proposal and the review thread.

(Bob Gilmore) #91
  • What is your evaluation of the proposal?

-1

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

No

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

No, I feel that we're generally favoring explicitness (and verbosity) in favor of shorthand sugars. Frankly I'm opposed to the current implicit return in one-line closures as well, but I (somewhat) sympathize with the motivations for them. No need to spread them.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Ruby user for many years, where implicit returns for all functions are common.

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

I'm opposed mainly due to my experience in Ruby, where I had to determine if the thing that I'm calling, and that THAT'S calling, and so on, returns anything. Sure we can look at the return values of those functions, but why NOT just keep it local and look for a return?

I think that this proposal is even worse. At least in Ruby, it worked regardless of the number of lines. In this proposal - want to add a print() call, or introduce a new variable? You've tipped from one line too many. Now you need to add that return.

This is exacerbated bu the existence of the Never, and how it can propagate through implicit returns.

1 Like
#92

Both functions and computed properties must have an explicit return type, so I'm not sure what you're talking about when you mention having to dig through all the called functions to find out what a function returns. Just look right there, at the signature.

(Maik) #93

Strong -1

In my opinion it makes code more complicated. It will be harder for beginners to understand and it might result in bugs because something might be returned by accident.

I might be wrong on this, but for me it feels like this could result in situations that were possible in Objective-C (and other languages) where you could return someList instead of return someList.count. The code worked but it was confusing for beginners and could result in bugs.

Everyone makes mistakes and I want the compiler to be help me to avoid mistakes I could make.

#94

That is not a concern. The only form of subroutine which allows type inference for the return type is a closure, where implicit return is already legal. For functions and getters, there is no chance to be confused as to whether a single expression is returning a value. If it's not, the compiler will complain.

This, too, is not a concern. The type of the expression must match the return type. In this respect, nothing is changing. The proposal simply asks that we be able to elide the return keyword. You still have to know what you're returning.

2 Likes
(Pierpaolo Frasa) #95

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
(Teva Merlin) #96

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.

(Gwendal Roué) #97

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
#98

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.

(Michael) #99

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
(Michael) #100

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

(Matt Rips) #101

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

1 Like
(Gwendal Roué) #102

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]