[Pitch] Last expression as return value

-1

I'm also in the "force always use self" camp. Much earlier in my career I was bitten by local variables shadowing UIViewController.view that were later "mostly" removed. This proposal seems to add exactly the same type of footgun.

The primary motivation for this is still avoiding typing in log statements. This is an annoying, but somewhat rare issue, for which easy workarounds exist by using closures.

I think the bar for implementing this needs to be "Explicit return statements are always less clear than implicit ones" because that's what we will see develop in codebases as a certain population of developers will migrate towards using this at all times.

6 Likes

Well, now I want to say this example should be allowed since it's so similar to the case I and many others want to allow in if, switch expressions. :man_shrugging:

But I'd hope we can keep it "Swifty" for normal functions to still use return foo. Is the solution to endorse the full pitch and just hope programmers don't overuse it? [1][2]

Or do we want the compiler to remain opinionated about this, still enforce the use of return foo in most functions somehow even though exceptions where return can be omitted are now numerous? I currently can't picture a policy that could do that but still allow the above example.

Yet I haven't liked any of the other compromises. So I, for one, have changed my mind and am reluctantly +1 on the pitch, admitting that I still think it's a footgun and hoping it use doesn't become commonplace.


  1. like I was personally hoping if let foo didn't get overused because that still looks meaningless to me, however I think I've lost that fight ↩︎

  2. unless they really want to ↩︎

1 Like

Leaving aside that returns mid function are not an error or failure or unusual in anyway...

I would have thought a safety first language would want explicit consent to move information from one scope to another? This seems a little... hand wavey.

One line returns always seemed like people not stressing about rolling through a stop sign on an empty side street. This seems... more cavalier.

13 Likes

I'm not familiar with how Kotlin handles this but does Kotlin error in this case? Swift currently (and would continue to under this proposal) warn.

func g() -> Int { 1 }

let f: ()->Void = {
  print("calling g")
  g() // Warning: Result of call to 'g()' is unused
}

Both before and after this proposal, the fix for the warning is either to mark g() as @discardableResult, or to write _ = g() in the function body.

Things are different without type context, of course, but given your example says "when a closure that must return void" I am assuming this is with context.

3 Likes

I'm neutral to slightly negative on this. Can I set my status to it's complicated? :')

As a relative newcomer to the language I think I should point out my first impressions of Swift was how much 'magic' syntax there was, as I think we quickly forget once we become familiar with a language. Some examples: closures as the last argument can omit parens, result builders, type inference on member lookup, if there's a single expression it's the return value.

All that to say that I think there's a bar of utility that new syntax and sugar needs to pass, as it adds to the total cumulative force that these special cases add. And in the general case (in a function) I don't see it. Writing return makes things explicit, and really isn't an issue.

With if and switch expressions I can see the value. There's opportunity to write bad code, but you can write bad code with a lot of tools and I don't think that should discount adding them to the language if they have a genuine usecase.

At this point, this magic does exist. And while I dislike implicit returns, never using them myself except in inline closures, if this gets added to if and switch expressions, you might as well add it everywhere -- the absence of the feature in standard functions becomes the exception at that point. So yeah, I guess my vote is 'add them everywhere, I won't be using them'. :slight_smile:

2 Likes

Status accepted :wink: and let me say I like your comment.

2 Likes

Instinctively, I feel strongly against this pitch, but I need to think about it more in order to elaborate a proper response, I'm not quite grasping the actual reasons why I'm against this idea, because the examples provided that should show the problems generated by omitting the return on the last expression don't seem too problematic to me.

But I'd like to at least quickly respond to this:

I disagree. To me, arbitrarily splitting a function because it's "too long" hurts code readability and clarity, forces developers to jump around the codebase to understand what a piece of code does, destroys local reasoning... it's just a bad idea.

The feature of omitting the return value from the last line in a returning function should stand on its own for arbitrarily long functions, but I think that, when reading a function, the type of the function, especially the return type, is something that the developer should keep in mind in general, so I'm not particularly worried about it. What worries me more is that seeing a bare expression (or just a value) at the last line without return would just look "weird" and not quite right, and would not communicate in any way that the value is being returned.

In a single-line function it makes sense to omit the return because a 6 character keyword plus space, in such function, doesn't really add anything to the meaning of the function code: it's obvious from the immediate context, and the return would be just unnecessary noise.

But in a large function, the return at the end actually adds something. Instinctively (again) I feel like omitting the return would be perfectly fine for functions (or do and other type of expressions) that are very short, like max 3 or 4 lines, but I wouldn't like it for longer ones: the utility of omitting return from single-line functions is, to me, due to the fact that the function is really small, not that it's specifically only 1 line long.

I know that a linter could be used, to not allow omitting the return for large functions, but I don't think the existence of linters should be considered an argument in favor of a language feature, if it happens to be undesirable in some cases. On the other end, the benefits of (optionally) omitting the return at the last line could make this point about linters more acceptable, so I'm finding a little hard to rationally justify my dislike for this pitch.

11 Likes

At the moment I'm somewhat torn on this proposal, but rather more against than for.

On the negative side, the idea that from now on the compiler will constantly be trying to return almost everything I ever write feels to me like a pretty big difference to the language. Without having experienced this feature first-hand it sounds both annoying and stressful (and yes, also convenient for temporary print statements and multi-statement branches of if/switch expressions). The annoyance would come from the constant error message informing me that my latest expression isn't of the correct type, and the stress would be in contexts where the return type is quite permissive (as discussed above). I haven't seen much in the way of assurances that this won't generally worsen error messages and build times.

On the positive side, it would solve the current issues with multi-statement if/switch expressions and I think that it might ultimately prove itself to me to be a natural and ergonomic extension of the language that I use all the time and am glad was pushed forward against my resistance.

I share @JuneBash 's feeling that I wish I could try it out for a while first. I don't feel that my imagination will give me an accurate sense of how it will be in reality.

I was and am very unconvinced by the word then as the keyword, but then very late in the game in the last thread the word bind was suggested which I liked a lot better. Again though, to me it seems possible that this pitch could prove itself to be a beautiful extension of Swift's ergonomics, in which case I would be sad that I didn't see it at the time and instead we got saddled with a new keyword.

I think I'll (re-)bring up a rather different approach to solve the problems at hand, but I'll do that in a separate post so that anyone who shares my sentiments here can like this post without seeming to endorse the alternative approach.

5 Likes

I understand that the ideas I'm about to put forward are unlikely to be made reality in their exact form since they're more, dramatic let's say, than the two proposals on the table (last-expression and keyword), but maybe they nonetheless bring some value by being different and triggering some other ideas. I did bring up both of the following ideas in the previous pitch thread.

The first "innovation" that I'm suggesting is to consider separately the two main reasons that we want multi-statement branches in if/switch expressions:

  1. Logging
  2. Factoring out intermediate bindings to reduce duplication or complexity

Maybe we could solve these two needs separately from each other, leveraging the unique aspects of each to arrive at a solution that is better for each one.

Logging

The unique aspect of logging that I'll focus on here is that the code that follows it does not depend on its presence, meaning that it can be encapsulated in a sub-scope. We could introduce something that allows us to run a group of statements without disrupting the declarative-ness of the surrounding context.

let predictedFavoriteNumber: Int? =
    if user.age < 5.years {
        user.birthMonth
    } else {
        run { print("user.age: \(user.age)") }
        nil
    }

Of course we would bike-shed the keyword.

Intermediate Bindings

In writing about my suggestion regarding intermediate bindings I realized that it's much less straightforward than my suggestion about logging, so I won't go too deep into it.

The main thing I was going to focus on is that I sometimes wish I could have the final expression above the intermediate definitions, because often the names of the intermediate expressions will be illuminating enough and I don't actually need to read their definitions.

I was imagining that it could look something like this:

func numberOfReviews(for product: Product) -> Int {
    reviews.count
    where {
        var reviews: [Review] {
            product.purchaserReviews + product.anonymousReviews
        }
    }
}

or a slightly more real-world example:

func listingsQuery(for ids: Set<ListingID>) -> SQL.SelectQuery {
   .init(
        selectComponents: listingColumnNames,
        tableName: "listings",
        whereCondition: whereCondition
    )
    where {
        var listingColumnNames: [String] {
            ["title", "createdAt"]
        }
        var whereCondition: SQL.BooleanExpression {
            ids.reduce(into: .false) { condition, id in
                condition = .either(condition, or: .equals("id", id))
            }
        }
    }
}
3 Likes

I want to voice a weak +1 for this pitch. Overall it's getting a plus one because it consolidates some use inconsistencies of when you do and don't need to use the keyword return. With it in place and consistent use, I also do see the value that Ben describes in the pitch of "return statements in the middle" as usefully highlighting an exceptional path when found in the middle of a block of code.

That said, with the Rust feature I've also found myself confused and unclear - the type inferencing sometimes works against you, especially as a new - or exploring - developer. Those moments where you think you know what you're getting back, but you missed some detail and are suddenly surprised that this isn't the type you're expecting. The real issue here, for me, is how this is surfaced by the development tooling, which I recognize is orthogonal in some respects to what a language design should be, but in practice - it really isn't.

When I do code in Rust with VSCode, and I'm not at all an experienced Rust developer, I rely heavily on the tooling to implicitly extend the visual code for me to show me what types are being returned when they're inferred. Nothing in the swift development world does this, and that already hurts in a lot of places - results of builders, trailing closures mixed in, or the combination of both. Tack on the flow with concurrency and task groups, and it can get even crazier.

I find new developers that I've helped following the same sort of "write it and try it" path, where there's a massive frustration boundary that this likely will add into - the same place that has some developers frothing in rage as the error/fixit message: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions appears.

So intellectually I like the consistency, and when it's there and working as this is pitched, I think it'll be nice to work with, but I fully expect the path between where are now, and the future idealistic locale, to be a very rocky road with sections washed out entirely by inconsistent developer experience in getting there.

5 Likes

I think this assumes that you're reading the function from top to bottom, which isn't necessarily the case. You might jump into the middle of it via the "Show Callers" command, for example.

4 Likes

Jumping somewhere into an unknown function is always a problem, that‘s why in Xcode and VS Code you always get the function head displayed no matter where you are inside the (maybe longer) function.

6 Likes

The context (the function signature) is shown in Xcode:

1   |
2   |
3   | func foo(x: Int) -> Double {
------------ scrolled down
40  |     1234
41  | }
  • note that this happens even when I put the function on several lines (in this case Xcode makes it a single line)
  • this signature is truncated if wider than the window size.

OTOH (as mentioned before), no matter whether that's "return 1234" or a bare "1234" – without seeing the full context you can't be sure it's actually a return from the function as it could be a return from some internal closure.

1 Like

I'm sorry I can't give you an explicit example right now, but yes, it usually occurs with code with context and not with simple case like this one.

By the way, is discardable result transitive ?

@discardableResult
func foo() -> Int {
  return 0
}

func withSomething<R>(block: () -> R) -> R {
  return block()
}

func f()  {
  withSomething() { // withSomething returns implicitly an Int. Should the compiler warn ? 
      foo()
  }
}
1 Like

Huge -1 from my side on implicit return at the end of function.

I love it in result builders, one-liner functions (which makes them concise in writing) and for simple return switch-cases, but using it in complex functions would bring confusion i.e. during code review where you have only a part of a code visible.

I am not convinced of using it that way and I remember it got me confused while writing in Ruby.

6 Likes

Continuing the tradition of plumping up vote size, I’m giving this a gigantic and gargantuan +1!

I’ve used implicit return in Ruby and Rust, and have found it easy and useful. I’ve very much appreciated the recent Swift changes to enable if and switch expressions, but long for a world in which that experience is more consistent and goes beyond single lines. The addition of do expressions will be a great, as will making implicit return universally available.

Yes, sure, it will take a bit of getting used to, just as any language feature does. But having adapted to implicit returns previously, I believe there’s a lot of fear mongering going on here. Those new to the concept will have to learn what an implicit return is, but it’s easy to describe. Those who already know about it will have to know that it’s now available in Swift. It’s not hard folks.

Implementing this feature will bring a useful shorthand to Swift. Even better, it’ll bring consistency to the current special-case implicit return carve-outs, making the language simpler and more elegant, and easier to learn.

4 Likes

I think this Pitch is worse than the previous (with the keyword then).

The title is quite misleading and much of the discussion so far is about omitting an implied return. In the first example of this proposal there is no keyword that is implied, and return (as the title says) is not a valid keyword before the last expression.

It is one thing to omit a keyword, a type, etc., when the compiler is smart enough to fill it in (at the convenience of the program writer and to the confusion of the program reader), and another thing to not have anything there in the first place. Beginner (or less lazy) programmers can always opt to explicitly write what could also be implied.

In the proposed syntax there is no such option. It is an un-named feature, with no option to make it more explicit. It implies that something is "returned", but we cannot say return there because this is not a function return, and we don't have any better name for this functionality, so we call it "return as last expression". This is totally confusing to both beginner and experienced programmers.

8 Likes

What about convey as a key word?

I proposed bind in the previous Pitch. Any keyword (also then) is better than no keyword.

5 Likes

I think named returns like we have for break and continue in nested for loops would have been nice, but I understand that to be a nonstarter for a number of reasons.

BTW: Can we drop the "I like it in Rust" argument. Because this is not what Rust has. Rust has the absence of a semicolon as a consent indicator which is weak sauce for sure, but it isn't nothing.

2 Likes