Use -> instead of curly braces for Single expression bodied property getters, subscripted getters and functions

As of SE-0255 developers can omit the return keyword for single-expression bodied getters for properties, subscripts and single expression bodies functions. This is a nice improvement to the language. Here's an even more improved version of the same proposal.

Existing Syntax for Properties

var location: Location { .init(latitude: lat, longitude: long) }

Proposed Change

var location: Location -> .init(latitude: lat, longitude: long)

Existing Syntax for Functions

extension Sequence where Element == Int {

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

Proposed Change

extension Sequence where Element == Int {

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

The return type of the function is automatically interpolated, which is similar to the type interpolation already available in Swift.

Existing Syntax for Subscripts

struct Echo<T> {
    subscript(_ value: T) -> T { value }
}

Proposed Change

With an implicit getter:

struct Echo<T> {
    subscript(_ value: T) -> value
}

The return type of the subscript i automatically interpolated, which is similar to the type interpolation already available in Swift.

With an explicit getter and setter:

struct GuaranteedDictionary<Key : Hashable, Value> {
    var storage: [Key : Value]
    var fallback: Value
    subscript(key: Key) -> Value {
        get -> storage[key] ?? fallback
        set {
            storage[key] = newValue
        }
    }
}

When you look at both existing and proposed change, the braces removed will provide even better readability and the code looks clean.

The proposed change also improves the semantics of the -> symbol for returning values across properties as well, expecially for getters.

The developers can still use existing syntax, if required.

6 Likes

Today, -> isn't an operator. It's a symbol for introducing a return type. What is the benefit to adding new semantics to this symbol?

In my view, losing the clarity that braces provide (namely, that what is in between is a code block) outweighs any perceived benefit gained from removing them. You aren't even saving keystrokes.

21 Likes

Why is there so much hate for braces lately? :(

the braces removed will provide even better readability and the code looks clean.

Your syntax is less readable, because you don't know where the function ends

24 Likes

In my opinion, the -> symbol will get more scope. Currently for developers who come from different language background like C, C++, C#, Java etc, the single bodied properties, functions will look odd (no return keyword added and is not preferred due to recent change). Even I'm from C# background.

As -> is already considered as the symbol for returning anything in functions, the same semantics will be extended.

When it comes to keystrokes,

  1. Properties will stil require same keystrokes but we'll avoid braces and they will be preferred more for multiline statements but not in single bodied properties and functions.
  2. For functions the number of keystrokes saved are equal to the property type name length.
1 Like

The functions will end in the same line as they begin, which is simpler for single expression bodied functions.

The reason for not requiring the return keyword in this case applies to the braces and more meaningful -> symbol. The developers can easily interpret that the function is single bodied with -> in between function and the body.

As a developer I believe every developer separates functions with at least line gap (above and below). The documentation added will also make the function separate from others.

1 Like

Even though I'd prefer braces, I think what he's proposing makes it more readable. Because, -> symbol is already having the semantics of returning the values or references from function. The same just applies to properties as well which provides more meaning.

Frankly, I didn't like the removal of return keyboard from functions single bodied expressions. But kumar's proposal supports better readibility.

1 Like

I disagree with this: -> has a very specific meaning in Swift, and the fact that in other languages is used differently is not a good reason for messing up with the language.

I personally think that the braces improve readability, but I'm not (in principle) against a change that removes the need for braces in single-expression function bodies, but not with ->.

Even a Kotlinesque = would be better, to me, like:

extension Sequence where Element == Int {
  func sum() = reduce(0, +)
}

But what's the point? To not spell the return type in the functions signature? This make the signature worse, not better: I would oppose this omission in a code review, it makes the code clarity worse, for a pointless gain of less characters used where they should actually be used, that is, in a function signature

Please consider that Swift is at the polar opposite of many other languages: Swift syntax and guidelines are in a way that allows for code to be readable like natural language even without the IDE support, and a function signature without the return type, in this context, is a terrible idea.

I agree with the subscript example, though. Because the subscript type is already clearly spelled out in the signature, the change would only cause to drop the braces in favor of something else. But for this very minor convenience I wouldn't bother.

16 Likes

The -> symbol introduces the return type. It does not introduce anything else. It has absolutely no bearing on what is returned. Consider -> Never, in which literally nothing is returned.

2 Likes

The only advantage I can see to this is that it looks like a feature from C#/javascript.

This is a tiny advantage that IMHO is massively outweighed by the fact that in Swift -> points to the type a function returns, and overloading it like this to also refer to how to calculate the value that gets returned is super confusing, especially given the difference in meaning of those two things is so slight but so fundamental.

This also looks particularly bad when you want to use your syntax to define a var that returns a function, possibly even ambiguous for the compiler.

Why should Getter get all the benefits, why not for Set as well? And why not for modify / _yield

The existing language is proper, request not to fix what is already fixed.

Please, I want to use a simple language, and not remember all these semantics.

5 Likes

This is not really an advantage at all.

6 Likes

In this case I totally agree, I don't even like it in C#. I only mentioned it because familiarity to users coming from other languages can be a good thing, and its a reason that has been highlighted in this pitch.

I'm not a fan of altering -> for this purpose and would prefer something like =. But more generally getting rid of {} seems at odd's with Swift's focus on control-flow clarity.

But after messing with it in Kotlin, I came to appreciate how clean it can be:

fun makeFullName() = "$firstName $lastName"

However what I see as it's best use case is functions that run/return functions.

// From this
fun makeAdder(amount: Int): (Int) -> (Int) { 
    return { it + amount } 
} 

// To this
fun makeAdder(amount: Int): (Int) -> (Int) = { it + amount }

This makes coroutines ultra clean when running in a function.

// without
fun main() {
    runBlocking<Unit> { // start main coroutine
         // code
    }
}
// with
fun main() = runBlocking<Unit> { // start main coroutine
    //code 
}

In some ways it can almost feel like a keyword or annotation, allowing some very fluent functional APIs.

func runOnlyInDebug(_ closure: @escaping () -> ()) {
    #if DEBUG 
    closure()
    #endif
}

func logSensativeInfo() = runOnlyInDebug {
    print(creditCardNumber)
    print(socialSecurityNumber)
    print(bankPIN)
}

I'm still conflicted about how it would work in Swift, but IMO there are more advantages than simply getting rid of a few characters or looking more like other languages.

1 Like

I think that very familiarity is the reason behind a pitch like this. I agree that familiarity is a good thing, and not just that, in fact Swift takes profound pride in using familiar names for constructs that could be discovered to be much more powerful than their "original" counterparts: if/else, switch, for, enum, struct et cetera should be familiar to people coming from the languages in the "C-like" family. For these I can 100% understand and get behind the idea of using familiar keywords: for example, Swift switch is not a simple "C-like" switch, but uses the same keyword, instead of something like match or when. In cases like this, you get both the familiarity (you can use the switch like a "classic" one) but also the progressive disclosure.

Also, for the more "swifty" parts, Swift draws a lot of inspiration from many other languages, for example Rust (a lot) and Haskell.

But using -> in the meaning intended in the pitch is neither a familiar thing in general (Javascript is a dynamic scripting language that has nothing to di with Swift, and C# doesn't really qualify as benchmark for developer familiarity, and there's nothing particularly good or inspiring about the syntax of both) nor could help the "progressive disclosure" learning pattern: it would actually be confusing because, as already said, the meaning of -> is "on the right of this you can find the return type".

An important point, in my opinion, is to consider that Swift doesn't really follow the "script-like" trend that many languages do (another trend is "pragmatic"): clarity and explicitness are king when dealing with maintainable production code, and Swift is well equipped here. This also means that, when actually trying to write scripts, Swift falls short due to verbosity and that very need for explicitness, but that problem can be tackled differently.

From -> RetType to ->/= RetExpr , why not just choose => RetExpr pattern in C#? And we can change .init(...) to .(...) drop 4 letters, result in -> .init(...) to -> .(...) :laughing:

I don't think omit {} worth much benefits for developers of Swift. They're already getting used to it, and like what's {} meaning for function/closure object.

-1 for me. I like the way using braces to denote an executable body in Swift is extremely consistent, and this is the same number of characters but just adds a new type of symbol to parse when looking at code.

7 Likes

Your initial post says that the only restriction is for single-expression bodied blocks, but the quoted comment says that the expression must begin and end on the same line—these are two very different things. Which one do you mean? Can you clarify if the following would be allowed?

func add(_ someNumber: Int, _ otherNumber: Int) -> someNumber
  + otherNumber
1 Like

Personal opinion: I've been forced to do C# for over a year (it's finally over) and I have to say the "=>" syntax for single expression methods is one of the things that I disliked the most. In my opinion it breaks consistency.

It's not that bad in the case of anonymous closures, but for functions and methods I found it really confusing.

2 Likes

In my opinion this would make the language more confusing and the problem solved is not a problem at all in my opinion. Why would we need multiple ways to write a function? Currently anybody picking up swift will know that this is a function:

func makeInt() -> Int {
    Int.random(in: 0..<100)
}

and that this is a computed property:

var someInt: Int { Int.random(in: 0..<100) }

I don't think that this

func someInt() -> Int.random(in: 0..<100)

or this

var someInt: Int -> Int.random(in: 0..<100)

is an improvement.

For the property we would keep the type but for a function it's dropped?

And then in a protocol we'd write this

protocol SomeProtocol {
  func makeInt() -> Int
}

Does that makeInt() return something? It doesn't because it's a protocol so Int is the return type but if I'm just learning the language that would look totally bonkers to me.

The get -> storage[key] ?? fallback looks completely alien to me to be honest and it's very out of place next to the set which does need the curly braces.

All in all I think this is a solution in search of a problem and in my opinion there is no problem in Swift that this solution should be applied to.

11 Likes

I like the idea behind this pitch, because my team has found implicit returns from functions confusing.

Some team members try to use implicit return every single time they have a single-line function, while other team members find the syntax confusing unless the function is short enough to fit on a single line anyway. If Swift had special syntax for single-line returns like JavaScript (or C#, Kotlin, and the other languages mentioned upthread), our style guide would recommend using that syntax when it makes sense, and explicit return otherwise (just like our team’s JavaScript linter already enforces).

However, I agree with other commenters that it’s confusing to overload -> for this purpose. @BigSur suggested the C#-style fat arrow => in jest, but I think it’s the perfect choice for a few reasons. (It’s also available in JavaScript.)

Overloading -> makes it impossible to explicitly specify the return type for functions, but not for properties (as @donny_wals pointed out). With => it’s still possible to annotate the type of you want to (or need to, to resolve ambiguity):

// Use the inferred type if you prefer
func someInt() => Int.random(in: 0..<100)
var someInt => Int.random(in: 0..<100)

// Explicitly specify the type if you prefer
func someBool() -> Bool => Bool.random()
var someBool: Bool => Bool.random()

protocol SomeProtocol {
  // Not ambiguous because => can’t be used in a protocol
  func makeInt() -> Int
}

@GetSwifty suggested the Kotlin-style =. That might work for functions, but it won’t work for properties because it collides with the stored property syntax. And even though it works for functions, it’s still confusing, because it’s not clear that the function body is evaluated every time the function is called, unlike properties that are called just once:

// Computed property, called every time
var sum: Int { reduce(0, +) }

// Function, called every time
func sum() -> Int { reduce(0, +) }

// Stored property, called once
var sum = reduce(0, +)

// Function, called every time (!)
func sum() = reduce(0, +)

=> doesn’t have that same confusion, and it can be used for computed properties:

// Stored property, called once
var sum = reduce(0, +)

// Computed property, called every time
var sum => reduce(0, +)

// Function, called every time
func sum() => reduce(0, +)

@Viranchee raised a good point, that this syntax shouldn’t be limited to getters and not setters in computed properties and subscripts. Thankfully, it doesn’t have to be limited, because assignment returns Void and that’s what set expects:

subscript(key: Key) -> Value {
  get => storage[key] ?? fallback
  set => storage[key] = newValue
}

-> would also work for set, so this isn’t an advantage for => specifically.

2 Likes