Omitting Returns in String: Case Study of SE-0255

Like I said, it’s subjective. I agree that let / in is the weakest in motivation. guard expressions would be useful though and would make sense with in following the else expression I think. And if I can guard let / in then there is a consistency argument for also being able to let / in as well.

I don’t know where the “in” is coming from, but you can use the ternary operator to model guard:

func doIt(_ x: Int) -> String {
  guard x < 0 else { “pos” }
  “neg”
}

// is the same as:

func doIt(_ x: Int) -> String {
  x < 0 ? “neg” : “pos”
}

For more complex expressions, I don’t see the benefit over the “return” keyword. It’s short, explicit and familiar to almost every programmer in the world.

I do not support change for the sake of change.

Of course you can use ternary but that’s besides the point. The whole topic of conditional expressions was revived precisely because a lot of people don’t like ternary and find it hard to read. You could also use an if expression here, but in some cases guard communicates intent a lot more clearly. That said - I did say that we should focus on if and switch expressions before considering guard and let expressions.

The in serves as a connective between the else block and the main body of a guard expression and is inspired by ML-ish syntax. I think placing the main subexpression immediately following the closing } would be awkward. FWIW, in would only be required by guard expressions, not guard statements.

1 Like

guard statements also do not have to return a value. They can throw or use one of the various abort functions. A ternary expression must return a value.

Thanks a lot for this, it's an excellent case study, and here's my two cents.


From a purely stylistic perspective, single line computed properties look ugly if the type is repeated both in type declaration and constructor, for example in

public var debugDescription: String { String(self).debugDescription }

doesn't look particularly good due to the String { String( portion: from a code style perspective, I'd favor something like

public var debugDescription: String { .init(self).debugDescription }

The __ Control Flow__ section shows the painful state we're in for the lack of if/else and switch expression, for example this

static func ==(
    _ lhs: _StringComparisonResult, _ rhs: _StringComparisonResult
  ) -> Bool {
    switch (lhs, rhs) {
      case (.equal, .equal): return true
      case (.less, .less): return true
      default: return false
    }
}

public func distance(from i: Index, to j: Index) -> Int {
  if _fastPath(_guts.isFastUTF8) {
    return j._encodedOffset &- i._encodedOffset
  }
  return _foreignDistance(from: i, to: j)
}

would be clearer if written like this

static func ==(
    _ lhs: _StringComparisonResult, _ rhs: _StringComparisonResult
  ) -> Bool {
    switch (lhs, rhs) {
      case (.equal, .equal): true
      case (.less, .less): true
      default: false
    }
}

public func distance(from i: Index, to j: Index) -> Int {
  if _fastPath(_guts.isFastUTF8) {
    j._encodedOffset &- i._encodedOffset
  } else {
    _foreignDistance(from: i, to: j)
  }
}

I’m going to stand up and disagree.

As a personal, subjective opinion, I find that omitting return from a multi-line function is undesirable and decreases clarity.

Even for a computed property with just a single expression, if it spans more than one vertical line of code, I would strongly prefer to include return.

Essentially, the *only* time I see any benefit from omitting return, is when the entire body of the scope is on a single line of code. In that case, the reader knows at a glance that there is only one thing happening, and it must be returning a value.

The moment the body spans more than one line, if return be omitted, suddenly the reader must consciously, cognitively parse the entire multi-line body to determine whether it is or is not a single expression, and that constitutes a reduction in readability, a loss of clarity, and an increase in mental burden.

5 Likes

I agree with this.

On a more general note, I don’t think that trying to turn Swift into an expression-oriented language incrementally is a good idea. SE-0255 was a misguided step in this direction; if and switch expressions, and now ideas like letin, amount to an attempt to override a core design decision of the language step by step.

Having an agenda isn’t automatically a bad thing, but it should be clearly expressed (hah) though a manifesto or similar.

2 Likes

The point is, if if/else and switch were expressions, than a function body starting with those keyword would be a single expression.

This case is similar to using a plain method instead: it would span multiple lines for readability, but it would still be a single expression, for example

static func ==(
    _ lhs: _StringComparisonResult, _ rhs: _StringComparisonResult
  ) -> Bool {
    Pair(lhs, rhs).fold(
      onBothEqual: true,
      onBothLess: true,
      otherwise: false
    )
}

public func distance(from i: Index, to j: Index) -> Int {
  _fastPath(_guts.isFastUTF8).if(
    true: j._encodedOffset &- i._encodedOffset,
    false: _foreignDistance(from: i, to: j)
  )
}

Yes, those are exactly the sort of multi-line bodies for which I am strongly opposed to omitting return.

1 Like

Why not? But there's no need for a full plan here, we could literally just turn if/else and switch into expressions and make a huge chunk of the Swift community happy, myself included: I write code that would greatly benefit from this on a daily basis.

Also, this is like normal, regular stuff in languages that partially compete with Swift, for example Kotlin.

2 Likes

I agree with this. Just because there has been discussion of some possible future directions does not mean there is “an agenda” or that these directions must be pursued. In fact I encouraged that we don’t pursue these directions right now because I think they will be too controversial. if and switch expressions would deliver enormous benefit. We should pursue these as soon as we can find somebody to work on an implementation.

And nobody is proposing that you be required to omit return here. This is a choice that should be left up to individual teams, not dictated by the language. I imagine linters would add a rule allowing you to ban return omission if enough people really don’t like it.

1 Like

That’s not the point.

Someone upthread claimed that if / switch expressions would make certain functions *clearer* by allowing the omission of return.

I am saying that in the examples given, omitting return would make the functions *less clear*. The change proffered as a benefit, appears to me a detriment. I am disputing the very premise of their assertion.

If you want to make a *different* argument for if / switch expressions, by all means do so (preferably in a new, dedicated thread for that purpose).

But the argument seen here, that they would enable people to write lengthy multi-line functions as a single expression, and thus omit return, thereby requiring readers of that function to studiously examine the entire vertical extent of its body in order to comprehend whether it comprises multiple statements or just a single expression, strikes me as a negative.

I agree that particularly long expressions can be difficult to parse in such situations. However, one often has method chaining, which is pretty easy to read when spread across multiple lines, especially if you use an editor like Xcode, which indents successive lines one more level. The chain is thus clearly subordinate.

At the moment, I am on the fence with regards to allowing (other forms of) multi-line expressions to omit return, but if it came down to a vote, I think I'd go with allowing it. If it's really that difficult to follow, I'd hope the original developer would think so, too. In addition, much of the benefit of omitting return is lost if the expression spans multiple lines anyway.

1 Like

There is one additional guarantee that could be had from such a feature:

let view =
    let body = someBigFunction(param1, param2, param3)
    in makeLabel(text: body)

This restricts the scope of the body variable to a single expression, in what might otherwise might be a long function.

Having said that, two = in one expression looks terrible (consider this on a single line), and I think we would get much of the same value from a do {} expression, or even calling an inline closure without any language changes.

1 Like

This is a great example of why local bindings are beneficial! Limiting scope of identifiers is always a good thing and doing that is clunky today because trailing closure syntax is the only option. I really like this example because it shows how supporting local bindings in expressions is actually most beneficial outside of the context of single-expression bodies where return may be elides. It most useful when you have a longer body with many statements.

However, I think this example actually would be better written with trailing local binding syntax (using with here instead of where since where in Swift is used with predicates and local bindings are not predicates):

let view = makeLabel(text: body) with
    body = someBigFunction(param1, param2, param3)

The reason I think this works better is that it maintains the direct syntactic relationship of view with the result of makeLabel.

For contrast, here is the clunky syntax we have to use today if we want to factor out a subexpression while restricting its scope to the immediate context (note: we even had to explicit type annotation):

let view: String = { 
    let body = someBigFunction(param1, param2, param3)
    return makeLabel(text: body)
}()
4 Likes

In my opinion, this seems like we're getting closer to wishing there was a clearer way to use immediately evaluated closures. If you didn't have to wait until the end of the scope to see that it is evaluated… it would do the job of with as you've described perfectly.