SE-0255: Implicit Returns from Single-Expression Functions

(Brent Royal-Gordon) #21
  • What is your evaluation of the proposal?

I support this proposal and think that it's scoped properly as it is. The single-expression rule will allow most cases that should be allowed, forbid most cases that should be forbidden, and—importantly—not feel as arbitrary as restricting this syntax to certain declarations would. Part of what makes the current design chafe is that it's so obviously an arbitrary rule; single-expression everywhere would feel much more fair and principled.

I also think that this is the kind of proposal that Evolution is prone to overthinking. I suspect that in practice, even people who feel in the abstract that uses in certain contexts might confuse them won't actually find them confusing when they encounter them in the wild. If accepted without revision, I really think that in a year most users will think this change was an unqualified improvement over the status quo.

(The one place where I might deviate from the proposal is in initializers—init?() { nil } reads strangely because you're not really expecting an initializer to return a value at all. On the other hand, if we ever get factory initializers, we'll probably want them to support single-expression implicit return, so it might be better to let that one slide.)

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

Yes. This is a tiny but very frequent annoyance; in aggregate, it's well worth addressing.

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

Yes. There's a direct analogy to the equivalent closure feature and Swift includes other shorthands for very short declarations, like implicit get in accessor blocks.

  • 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 several languages like Ruby and Perl where everything is an expression. I don't favor that approach because you often don't realize what you're returning; I especially hated the Ruby style rule discouraging explicit return unless you were returning early. I think this proposal strikes the right balance between clarity and brevity.

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

Just a glance at the final proposal, but I read the pitch more closely.

7 Likes
#22

I think this would fit perfectly with the language and would save me from frequently writing functions and computed properties without the return, expecting exactly this to work already. This makes things both more concise but also more consistent in my mind.

Much more speculatively (and therefore not a major consideration), this proposal might also open a door for solving some other small-but-frequently-discussed annoyances in the language. e.g. there have been recent discussions in the pitch thread for if-else expressions (where if-else statements are expressions if all the clauses are single expressions) about extending that idea to switch statements as well, and implicit returns for single-expression functions would give you a nice way to write functions on enums where the body is just a switch statement. Roughly:

enum GameResult {
  case loss, draw, win
  
  func points() -> Int {
    switch self {
      case .loss: -1
      case .draw:  0
      case .win:   1
    }
  }
}

As I said in the pitch thread, I'm really not a fan of the idea of adding a whole new syntax with no clear benefit (it doesn't seem to eliminate any ambiguity for me). If a concise syntax is desirable, then I think this should be:

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

which achieves the goal of allowing “one liners”, so I'm not sure what failure you are talking about.

Pitch: if/else expressions
(David Keck) #23

I’m -1 on this proposal.

In my opinion this proposal amounts to a style preference that is being sold as a consistency fix. In reality this will result in less consistent code across Swift as a whole with some functions containing return and some functions not. So, no. This isn’t even a problem IMO.

No, Swift is attempting to be friendly to newcomers and functions that have a return sometimes but not others (while still having a return value in some cases) will be confusing at best and frustrating at worst. Closures already have this implicit return, but newcomers can and do shy away from them due to their difficult to parse syntax.

N/A

A quick reading of the pitch and proposal.

2 Likes
(Matthew Johnson) #24

As I mentioned upthread, I personally don’t find this syntax to be advantageous at all. The current proposal supports “one liners” just as well as this syntax does (unless you are following a style guide that is too rigid about the formatting of single-expression bodies when they must be surrounded by braces).

If we are going to introduce a whole new syntactic form for single-expression syntactic sugar I think it should go further and also support return type inference. We support property type inference already and this would be quite similar:

let x = 2
let doubled = x * 2
func double(x: Int) = x * 2

However, this obviously does not work for computed properties, as I pointed out upthread. So I think we would need to use syntax that is different than = in order to make it work with computed properties, perhaps := (as a strawman syntax):

// This is a computed property.
// Compared to the syntax in the current proposal (below) it is shorter mostly because 
// it supports type inference, which would be even more beneficial when the type is large.
let doubled := x * 2
let doubled: Int { x * 2 }

// This is a function with an inferred result type.
// Compared to the syntax in the current proposal (below) it is shorter mostly because 
// it supports result type inference, which (as above) would be even more beneficial 
// when the type is large.
func double(x: Int) := x * 2
func double(x: Int) -> Int { x * 2 }

I’m guessing return type inference would be a non-starter. The only other benefit this syntax has is that it may feel lighter weight to some people (it does a bit to me). However, I think that is not a significant enough benefit to justify deviating from consistent use of braces.

Return type inference (and type inference for get-only computed properties and subscripts) might be enough to justify that deviation if it was on the table. However, it still feels orthogonal to this proposal. It is not in conflict with the proposed syntax which increases overall consistency in the language. The first thing I wanted to do when I learned about single-expression closure shorthand is to be able to use it in other code blocks. I’m sure I am not alone.

This rationale makes some sense from a language designer’s perspective. But from a language user’s perspective it feels inconsistent to limit it to this context. Swift will be an easier language to learn if the rule is just “you may omit return from a body that consists of a single expression” than it will be if you must append “in these specific contexts” to that rule. It’s much harder to remember a list of contexts in which the sugar is allowed than it is to just need to remember the “single expression” rule.

4 Likes
(Jens Ayton) #25

A possible benefit of “equation syntax” is that it could be used to forward properties, and perhaps subscripts:

class HygenicViewController: UIViewController {
    private let label: UIView!
    // ...
    public var title: String = label.text // Eliding the issue of VC lifecycle here

    // Using = for a mutable relationship doesn’t sit great with me. Maybe this?
    public var title: String = &label.text
}

This desn’t really feel like a winning argument to me, though.

(David Waite) #26

In my opinion, there is often a confusion in languages that support closures due to recycling of curly braces - does { code() } indicate a block of code that is executed in context, or a closure that may be executed later?

In the case of func sum() -> Element = reduce(0,+), I wouldn't even suspect there was an ambiguity; that the sum function is being assigned the result (assumably a closure?) of the reduce(0,+) function call.

(Garth Snyder) #27

+1. There are some risks, but I don't think this would be as earth-shattering as many seem to believe. In particular, I don't think the proposed change would prove confusing, even to newer users. People are already familiar with this convention through closures.

If nothing else, I'm strongly +1 in favor of implementing the property getter portion of this proposal. It makes perfect sense and would reduce clutter in regions that really ought to look like a series declarations rather than body code. All the arguments in favor of auto-returning single-line closures apply equally well to getters. And single-line getters are more the rule than the exception.

It isn't a huge problem, but it's one of the lingering, niggling points of Swift syntax. I never write a getter without thinking, "Why do I have to put an explicit return here?"

Yes. This feature is already settled syntax in the case of closures. This proposal is just the dropping of the other shoe and a removal of inconsistency.

My reference point is Ruby. Ruby's system works fine (well, more than fine; it's quite nice), but I'm not sure that's really all that relevant here. I do like that this proposal potentially paves the way for expression if-else and switch. But even if those changes are never adopted, the proposal is still valuable on its own.

I participated in the several discussions and read the whole proposal. And as I mentioned, this is something that has been on my mind for a while.

(Michael Ilseman) #28

Strong +1 to Jordan's reasoning. Get-only shorthand syntax delivers the vast majority of benefit without any loss of clarity.

2 Likes
(Goffredo Marocchi) #29

IMHO, if the closure / getter is doing non-trivial work (e.g.: the body is more than one line) having implicit return is not helpful, but detrimental to readability: it would be a convention that the getter returns the value produced by the last statement of N statements implicitly.

:/...

(Michael Ilseman) #30

That's not being proposed. This proposal is only for single-expression bodies, similar to closures.

1 Like
(Goffredo Marocchi) #31

Then +1 to Jordan’s points and yours too :).

1 Like
(TJ Usiyan) #32

I don't see how the type is farther away from a single expression for a function than it is for a property, get only or otherwise.

1 Like
(Jarod Long) #33

I'm +1 on this. I think there's value in applying this rule consistently throughout all returnable contexts. It's easier to understand and remember "a single expression can be returned implicitly" than "a single expression can be returned implicitly, unless it's inside context X Y or Z".

I'm all for stripping away noise when it doesn't create problems, and I don't share the concern about reduced clarity that others have. Given that Swift requires explicit declaration of return types and enforces properly returning from functions, it's hard to imagine a scenario where it's difficult to deduce what's going on when this feature is used, even if you haven't encountered it before.

I think it's worth it. It's not a major issue, but it's not a major change either.

I would argue that it does given that implicit single-expression returns already exist for closures -- it's an extension of an existing philosophy.

I've used the feature in Ruby, which more permissively allows implicit returns regardless of the number of expressions. I really liked the added expressivity, but it could create some ambiguity when reasoning about what was returned from a more complicated function. That issue doesn't apply here given that return types are explicitly defined and enforced by the compiler, and the proposal only covers the very straightforward case of single-expression functions.

A quick reading of the proposal and this thread.

(Jordan Rose) #34

I don't even look at the type for a property; I just assume it's non-Void because Void properties are so rare.

1 Like
(TJ Usiyan) #36

Fair enough…

I don't see how the declaration is farther away from a single expression for a function than it is for a property, get only or otherwise.

(Preston Sumner) #37

After considering the counterarguments, I find myself in favor of the proposal. People are always going to want shorthand for writing one-liners because the conciseness of the code naturally demands an implied return behavior. If this was an alternate universe where people could modify the Swift language via a community library, I think it would become common sugar used by a lot of people.

// Many users will always want this.
func sum() -> Element { reduce(0, +) }

// They'll always wonder why they have to write this when
// the conciseness already implies a return behavior.
func sum() -> Element { return reduce(0, +) }

It doesn't seem to me that clarity is lost by the absence of the return statement because the nature of it being a one-line, single-expression function with a return type already conveys its intent.

A counterargument might be that, if this proposal is implemented, people might expect to be able to do this for other returning {} bodies, specifically guard. However, guard just doesn't read naturally when written like that. It really only makes sense with a return. A function has the benefit of the return type being spatially close enough to the expression to convey that meaning.

func contrivedExample() -> Int {
    // Guard that something is valid or else...5? Huh?
    guard somethingIsValid else { 5 }
    ...
}

To summarize my support for this proposal: as a spoonful of sugar, I can live without it, but if it was implemented, I would happily use it for probably all my one-liners.

As an aside, this proposal seems like a convenient marker that a language style formatter might take into account if configured to do so: if the function body is a short, single expression, format it as one line with the return removed. Otherwise, format function bodies with proper newlines, indentation, and an explicit return regardless of the number of expressions.

3 Likes
#38

I'm against this proposal (or in SE term, a -1).

I agree that functionsclosures, but also function declarationsclosure expressions.

IMO the proposal would make it less consistent now that the single-line declaration and multi-line declaration works differently.
Declaration doesn't need concision as much as expression does, and so I'd prefer that they are consistent among themselves.

Also function declaration and closure expression are used at entirely different places. You use function's name, and closure expression, where closure is expected, and declare the function elsewhere. So trying to make them consistent with each other, to me, is a red herring.

1 Like
(Chris Lattner) #39

The primary argument for this seems to be consistency with closure syntax. If that is a strong motivation, then wouldn't it make sense to either apply the exceptions to closures as well (which could have adverse effects on type checker time) or remove them from this proposal for functions?

(Matt Rips) #40

CLARITY: True enough that clarity is reduced, but to what degree?

func square(_ value: Int) -> Int { value * value }

struct Greeter {
    var name: String
    var friendlyHello: String { "Hello, \(name)." }
}

For an experienced programmer, the reduction is slight, and the overall intent remains obvious.

For the novice, the return statement would be helpful, but there is a hidden benefit. Once the novice is taught or intuits the intent in the context of functions or accessors, he or she will have a huge leg up when it comes to learning the syntax of map, etc.

EXPENSE: @nate_chandler - Is the burden on parsing and type-checking any greater than with standard closures? In absolute terms, how cheap or expensive are the special cases?

BENEFICIAL CONSISTENCY: Your comment echoes Emerson, "A foolish consistency is the hobgoblin of little minds," and leads me to question the value of this particular consistency.

Usually, there are good reasons for one case to be inconsistent from another. And, here, there is a good reason as to why the inconsistency was created in the first place. (See, above, "the entire reason we sugared this for closures in the first place is that there are cases where the return keyword obscures the intent of the closure because they are otherwise very small.") But, over time, experience has come to show that providing this particular special sugar in one place has created the unintended expectation that it would be available in nearly identical other places.

I'm on safe ground asserting that, for many users, there is little or no distinction between a single-expression closure, function or getter. They all consist of a single expression bounded by curly braces. Those users know that, sometimes, the return can be omitted. But, they don't know why it can be omitted some places but not others. It feels arbitrary. When they omit it in the wrong place, the compiler sets them straight, but, still, they don't know why. So, they feel confused and doubtful and hesitant. Even for users with a deeper understanding of the language's constructs, the same sort of doubts creep in, "Can I omit it here, or not?" (See @GarthSnyder's fine post, above.)

To my eye, this proposal supplies a helpful consistency. The main benefit is a reduction in cognitive load (not having to remember special cases). A second benefit is the aid to new users learning the syntax of single-expression closures. A third benefit is, for those who want it, a stylistic sugaring of their code. On the other side of the ledger, it seems that the cost is the same as the cost that we already pay for the same sugar in closures.

To answer the last bit of your challenge ("... need to be unified, but others shouldn't"), this proposal is scoped as it is. The authors are not speaking to any other inconsistencies, existing or future. Certainly, it would be unfair to ask that they address the state of consistency across the rest of the language, and the success of this proposal should not hinge on the same.

6 Likes
(Matt Rips) #41

I think the exceptions already are baked into the parsing of closures. The following seems to compile as intended, without an implicit return.

let fooClosure: () -> () = { print("foo was called") }
fooClosure() 
// prints: foo was called