Avoid curly brackets on single line bodies and computed properties

As titled, coming from Kotlin, this is hurting my eyes

public var elements: [Bool] {
    return [x, y]
}

public func makeIterator() -> IndexingIterator<Array<Bool>> {
    return elements.makeIterator()
}

public var debugDescription: String {
    return String(describing: type(of: self)) + "(\(x), \(y))"
}

public func hash(into hasher: inout Hasher) {
    hasher.combine(SGLMath.hash(x.hashValue, y.hashValue))
}

public static func ==(v1: Vector2b, v2: Vector2b) -> Bool {
    return v1.x == v2.x && v1.y == v2.y
}

public var r: Bool {
    get {
        return x
    }
    set {
        x = newValue
    }
}

I'd love to have the possibility to simply skip curly brackets for single line bodies and computed properties

public var elements: [Bool] = [x, y]

public func makeIterator() -> IndexingIterator<Array<Bool>> = elements.makeIterator()

public var debugDescription: String = String(describing: type(of: self)) + "(\(x), \(y))"

public func hash(into hasher: inout Hasher) =
    hasher.combine(SGLMath.hash(x.hashValue, y.hashValue))

public static func ==(v1: Vector2b, v2: Vector2b) -> Bool =
    v1.x == v2.x && v1.y == v2.y

public var r: Bool
    get() = x
    set {
        x = newValue
    }

I tried to search but I didnt find anything about

In your example, for the elements var, both declarations are valid but with different semantics

  var elements: [Bool]
  {
    return [true, false]
  }

… is a readonly var, whereas

  public var elements: [Bool] = [true, false]

… is a readwrite var, initialised at declaration, which can also be shortened to

  public var elements = [true, false]
4 Likes

With the recent change to allow implicit returns from single-expression functions, you can already freely write these as

public var elements: [Bool] { [x, y] }

and similar, which is very hard to beat for concision. I can't see the point of adding another syntax for this.

10 Likes

If it's not in an extension, you can make it read-only, as a stored variable:

public let elements: [Bool] = [true, false]

The braces (on a var) distinguish between a stored property and one that computes on demand but doesn't use up storage.

I deal with the same kind of frustrations (in both directions) when switching between the Swift and Kotlin.

As others have mentioned, some of your examples can be optimized but not others. My perception is removing much more syntax in Swift will encounter significant pushback from the community (see SE-0257: Eliding commas from multiline expression lists - #190 by Braden_Scothern and Allow function definitions to omit parentheses if no parameters - #5 by allevato )

One thing to keep in mind as a (non-satisfying) reason is how Kotlin treats everything as an expression, while Swift has semantic differences between Properties, Functions, Methods, and Closures differently. That, along with a more complex memory-management system I believe means some of the syntactic sugar you get for free in Kotlin is significantly complex to implement in Swift.

Swift is also designed for clarity and safety, while Kotlin sacrifices some amount of those for a more flexible and script-like syntax. So although I would love to do things in Swift like this:

func add(_ left: Int, _ right: Int) = left + right
func add1(to value: Int) = add(value, 1)
func addIf0(value: Int) = switch isAllowed {
    case 0: add1(to: value)
    default: value
}

there are reasonable arguments that it's much less clear what's happening, and therefore less safe.

Anyway, my guess is that type of syntax isn't coming to Swift anytime soon, just like static properties/methods, enum associated values, and turnery statements probably aren't coming to Kotlin. :disappointed:

6 Likes

As noted by @GetSwifty, there are substantial differences between Swift and Kotlin that make harder for Swift to push syntactic sugar too far.

Fortunately, the proposal linked by @jawbroken already makes easier on the eyes single expression functions, improving consistency between named functions and lambdas.

That being said, I disagree with the improvements in readability and understandability achieved by replacing {} with =: I think it's a lot clearer to wrap the body of a function in curly braces than to "assign" a single expression to the function. But even if it wasn't, I don't see profound differences once you can skip the return, which is to me the real offender. For example, I don't see how

public func hash(into hasher: inout Hasher) {
    hasher.combine(SGLMath.hash(x.hashValue, y.hashValue))
}

is hurting your eyes more than the = counterpart. Something that could be added is the type inference on computed properties, for example:

public var debugDescription {
    String(describing: type(of: self)) + "(\(x), \(y))"
}

would be better, the : String is redundant (from a readability standpoint), but something similar is achieved with .init, like:

public var debugDescription: String {
    .init(describing: type(of: self)) + "(\(x), \(y))"
}

so a nice balance was achieved by the aforementioned proposal.

Auto-formatting would always put that on new line and consume 3 lines instead 1 (for shortest bodies).

Of course it'd be optional, like in Kotlin, so that if you like wrapping always between brackets, you can continue doing so

It's not the fault of the language if whatever auto-formatting tools you are using are mangling your code in ways you don't prefer, and I really don't think that new redundant syntax should be added just to stop them from doing that.

5 Likes

Kind of, since I'd like to have the possibility to skip the brackets completely

My observation was in reply of one the quoted comments suggesting the "workaround"

Could be:

public var elements: [Bool] <- [true, false]

And of course, the type could be inferred here also. Though maybe that is going too far.

public var elements <- [true, false]

I'm not very keen on this - purely from the point of reading code, it makes it somewhat unclear whether you are assigning a function/expression or the result of executing the function to the variable. Obviously you can tell from the type and, sometimes, the body, which it is - but I'm not sure the extra effort required and potential for confusing people new to the language is worth it.

I find...

public var debugDescription: String = String(describing: type(of: self)) + "(\(x), \(y))"

...to be the most potentially confusing. Surrounding the code in brackets makes it totally unambiguous that this will be evaluated every time you call debugDescription, rather than...when the object is instantiated? When it's first called? Every time?

4 Likes

What you describe here is a var that has an initial value, but which can also be assigned to; it will be evaluated once for the initial assignment.

On the other hand :

public var debugDescription: String
{
  return String(describing: type(of: self)) + "(\(x), \(y))"
}

… will be evaluated every time it is called.

public lazy var debugDescription: String = String(describing: type(of: self)) + "(\(x), \(y))"

… will be evaluated once only, on first call.

1 Like

Just to be clear, Kotlin doesn't replace brackets, it allows you to use either

Given Swift's opinionated standardization, I think I agree allowing = in a function definition is confusing and easily abused. However I also find it quite elegant in Kotlin to do things like

fun makeFullName() = "$firstName $lastName"
// or 
val fullName: String get() =  "$firstName $lastName"

I especially like it when using when, e.g. for an enum:

fun makeSizeLabel(size: SizeEnum) = when(size) {
    SizeEnum.SMALL -> "Small"
    SizeEnum.MEDIUM -> "Medium"
    SizeEnum.LARGE -> "Large"
}

Yes you can look at it as muddling the definition of a function/method, and I used to agree. What changed my mind for things like the above is taking it to mean "makeFullName() = ThisExpression".

This is why Kotlin's "everything is an expression" construct IMO means it makes sense in Kotlin, and may not make sense in Swift. I don't think there's an existing analogy you can intuit for what func thing() = would mean.

Still, personally I would still like a way for single expressions to be written more concisely in Swift. Maybe something like

func fullName() -> String in "\(firstName) \(lastName)"
2 Likes

I really don’t see how this form is more concise than

func fullName() -> String { "\(firstName) \(lastName)" }

also, conciseness isn’t everything, I like concise, expressive code, but not a the cost of potential ambiguity and less approachability.

Still, I like the idea that things like if and switch should be expressions (not everything, though, for example guard is great as it is, and it’s one of the things I miss the most in Kotlin): if they were, you could drop the ‘return’, which seem to me the real offender to me, that is, a lot of additional characters that are redundant and add noise.

1 Like

Ditto!

The more concise way would be

func fullName() in "\(firstName) \(lastName)"

but my guess is an inferred Type for a function result would see a lot of pushback.

A couple brackets isn't a huge deal, but that's also true of parens for if/switch statements among other things. It wouldn't be earth shattering, but I would prefer the ergonomics in some cases.

1 Like

I guess you have a valid point there..

It's indeed pretty plain and natural extracting the return at the end

return if(cond) a else b
return when(cond) {
    a -> x
    b -> y
}

Regarding guard I find the concept really fascinating, but the implementation quite poor..
having to explicitly type every single time else { ... return } is quite verbose and annoying.. I'd have expected a default nil for Optionals and plain return with Void (maybe passing a string in a autoclosure)

Once you understand what's happening, I agree it's verbose. However, if you're new to programming or new to swift, I don't think it would be possible to intuit what's happening. If I came to Swift and saw

func doAThing() -> Thing? {
    guard thing.isReady 

    return thing.doTheThing()
}

I would find it difficult to guess what's happening.
v.s.

func doAThing() -> Thing? {
    guard thing.isReady { return nil }

    return thing.doTheThing()
}

IMO it's much clearer and easy to reason about what might be happening, and makes it clear that author's intention is to return nil if thing isn't ready.

1 Like

I dont know, for me the keyword itself guard is quite significant and self-explanatory

But maybe it's because I tend to associate it to a sort of a runtime assert

Implicitly retuning nil for functions returning ‘Optional` would be really bad to me, and profoundly contrary to Swift core values and patterns.

I guess returning implicitly for functions returning void would be fine, but a little confusing for newcomers.

3 Likes

Having to explicitly write the else branch empty the usage of guard in the first place, for me

1 Like