Initializer sugar

Reading through previous discussions mentioned by @letan, how is your approach different from this (besides allowing for let / var binding)?

Approach: Extract the members from the parameters of an initializer decl
This would give us something like this:

class C : Derived {
	// members come from the memberwise init.
    memberwise init(x: Int, y: Double = 4.0, z: String = "foo”) {
    	// your custom code can go here
        super.init(q)
    }
}

pro) Syntactically very terse.
con) This only supports one memberwise init.
con) Two ways to declare stored properties
con) Conflates the property grammar (behaviors etc) with parameter grammar, many of the same problems as approach right above does.

Core team was strongly opposed to this approach.

Speaking from experience, syntactic sugar is subjective.

It looks like Kotlin doesn't guarantee definitive initialisation, at least if I'm reading this correctly:

During construction of a new instance of a derived class, the base class initialization is done as the first step (preceded only by evaluation of the arguments for the base class constructor) and thus happens before the initialization logic of the derived class is run. It means that, by the time of the base class constructor execution, the properties declared or overridden in the derived class are not yet initialized. If any of those properties are used in the base class initialization logic (either directly or indirectly, through another overridden open member implementation), it may lead to incorrect behavior or a runtime failure. Designing a base class, you should therefore avoid using open members in the constructors, property initializers, and init blocks.

So I don't think the comparison is really fair, because it is easy to have a simpler-but-incorrect initialisation model, if that is your preference. It also has its own set of odd constructs to mix initialisation blocks with property initialisers, resulting in a fair amount of special syntax without obvious brevity or clarity benefits. I guess I'm not as enamoured with it as some others in this thread.

That’s true of all Java like languages, it is a mistake they made and you have to live with that in these languages because it is now too hard to change. For Swift:

class User(let name: String, var points: Int) { ... }

Would behave identically to:

class User {
    let name: String
    var points: Int)
    init(name: String, points: Int) {
        self.name = name
        self.points = points
    }
    ... 
}

It’s purely sugar.

1 Like

It's somewhat related, because I would guess that the complexity in class initialisers is part of the reason why their initialisers aren't currently synthesised. You already have the basically equivalent

struct User { let name: String; var points: Int }

and some tweaks to synthesised initialisers, as explored in the various memberwise initialiser threads, could close most of the remaining gaps here. At that point, I don't see what the Kotlin syntax really buys you, especially since it requires its own set of special cases for anything more complex than your example.

Or properties for internal work, like caching, that you shouldn't publicize.

Which special cases are you referring to?

It's good to see this topic come up again! I think following up Equatable, Hashable and Codable synthesis with more flexible initializer synthesis would be awesome! In case anyone is interested, I wrote a summary of my thoughts at the end of the lengthy discussion and review process. My views on the topic continued to evolve during the review itself as evidenced by that post.

I haven't given this deep thought since my proposal was deferred. A lot has changed since then in Swift as well as the evolution process so I'm looking at the topic with fresh eyes. Some things I might consider doing differently this time around (a lot of this overlaps with the core team's common feedback and points provided in the deferral):

  • The core team provided a detailed analysis of options in their feedback. Anyone wishing to contribute to this thread should be sure to read the approaches discussed section of that post. It lists pros and cons (as they saw them) of 8 different approaches to solving this problem, some of which have already been mentioned in this thread.
  • It might makes sense to focus exclusively on structs. The current memberwise initializer only works with structs and synthesized Equatable and Hashable also only work with structs. The definitive initialization model for classes introduces nontrivial complexity that must be addressed when classes are supported.
  • Behavior should be explicit. By the end of the discussion of my proposal I agreed with commenters that the "automatic model" which relied on property eligibility rules has too much potential to cause confusion. If there were more time before the review it is likely that I would have produced a new draft that adopted the "opt-in model".
  • It may not be desirable for parameter order to depend on source order of properties. There was a lot of discussion about the impact of re-ordering properties on the order of initializer parameters. The tradeoffs in this area should be weighed carefully. One middle ground would be a design that lets requires code to opt-in to property-order or use an alternate mechanism to explicitly control the order of initializer parameters.
  • Support for let properties with default values is important. I advocated for this from the beginning of the discussions but was not able to propose a change to the model for let properties in the original proposal. The core team provided some guidance that this is an important design decision and there are some options they are willing to explore to support this (or at least were at the time).
  • More general features should be considered. One of the most prominent pieces of feedback my proposal received is that it memberwise initialization is a very specific corner of the language that may not warrant a lot of feature complexity on its own. It might be better to explore features that can be composed to concisely build memberwise initializers instead of targeting them directly. I explored some options in this area following the conclusion of the review of the original proposal. The result of my analysis can be found in an appendix to a proposal I wrote for partial initialzers. I also explored the concept of "property lists" as a potentially more general feature that could be leveraged to define both property inclusion and parameter order for memberwise initializers.

Before we go too far down the rabbit hole however, the first question I have is whether anyone is interested in working on implementation. This is a very deep topic that could result in another thread hundreds of messages long without adding a lot to the previous threads. I'm not sure that discussion is worthwhile without people willing to do compiler work now that prototype implementations are required to move a feature forward.

1 Like

e.g. Randomly scattered init blocks, seemingly unlike any other part of the language, that are aggregated in source order to produce the actual initialiser. There's no great place for the accessibility modifier or annotations for the initialiser itself, so you get forced to spell out constructor and lose all brevity benefits (resulting in a soup like internal class Customer private constructor(name: String)). Things like that, resulting from breaking the rules about how things are nested in the language generally.

1 Like

These are largely a seperate issue only loosely related to short syntax for init, the equivalent in Swift is:

let x = {
    var temp = X(...) // Creat a temp.
    temp.... // Initialise the temp.
    return temp // Return the temp.
}()

They are not common in Java/Kotlin/Scala, or Swift.

I don't think that is really equivalent to Kotlin's init { … } because that appears to be designed to run arbitrary code, not just to initialise instance variables, as in the example from that page

class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)

    init {
        println("First initializer block that prints ${name}")
    }

    val secondProperty = "Second property: ${name.length}".also(::println)

    init {
        println("Second initializer block that prints ${name.length}")
    }
}

And Swift won't actually let you write code that depends on source order like this anyway:

struct C {
  var name: String
  var firstProperty: String = { return "\(name)" }() // error: instance member 'name' cannot be used on type 'C'
}

And the only way to run arbitrary code would be to make a bunch of private Void properties or some similar hack:

struct C {
  private let _ignore: Void = { print("test") }()
}

The biggest use case in Kotlin/Scala is the simple class Customer(name: String) { ... }, why not just support that and over time depreciate:

struct Customer {
    let name: String
    ... 
}

Which is a syntax anomaly. It looks like you have to do:

var c = Customer() // There are no `inits` so not obvious that you pass an argument.
c.name = ... // Do the initialisation separately, like you would in C.

Which I know you can't do, but it looks like you should because it looks like C.

PS to the @jawbroken quote at the start I added { ... } and removed internal so that all the examples in the post had the same form. This doesn't materially change my point.

A general remark: Trying to make an idea look stupid just because it has been invented elsewhere imho doesn't serve us well.
Most concepts in Swift have been implemented before, and when you want to copy from the best, you
have to know who actually is the best...
It may still not always be possible to adopt superior alternatives due to compatibility concerns, or because they just don't fit - but we still should honor that other languages have their strong points as well.

The "set of special cases" actually contains just two elements, which I'd reduce to one, because

isn't a special case for me, but rather a feature - the language doesn't force you to "randomly scatter" init blocks, it just allows you to place them where you think they belong (just like Swift does).

That's a special syntax for special needs, and imho not worse than private(set) public var x: String, or the same "soup" with a "{" sprinkled in it.
Even if you are forced to use this verbose spelling often (which most likely isn't the case), it's wrong that you lose all brevity benefits:
The modifier just has a different place than in Swift, and you don't write anything that you wouldn't have to write in Swift.
On the other hand, you can still avoid three repetitions of each member name, which you just can't do with our syntax. The syntax also opens a simple possibility for forwarding of protocol conformance (https://kotlinlang.org/docs/reference/delegation.html), which Swift still lacks, and possibly always will.

I wish you would retract this strange allegation, and I won't bother replying to the rest because it doesn't seem in good faith.

I don't see why both couldn't be valid? The first one allows you to have different parameter names at the call site, while the second would use the default variable name (minus the .self).

That would mean this is valid:

struct Foo {
    let name: String
    var points: Int

    init(fooName self.name: String, fooPoints self.points: Int) {}
    init(_ self.name: String) {
        self.points = 0
    }
}

I also think this kind of syntax makes logical sense since the second name is always the variable used inside the function, specifying self should be a natural extension of existing functionality to mean use the variable on self instead of a new variable.

I'd eventually like to see things like this being possible
struct Foo {
    var name: String
    // initializers...

    // Uses self.name as the default argument, otherwise, mutates self.name to the parameter value
    mutating func hello(_ self.name: String) {
        print("Hello \(name)")
    }
}

I really don't like this syntax. IMO it makes the initializer declaration far too important to the struct as a whole. This type of syntax would be confusing in everything except the simplest of structs. An object's variables should still be declared in the struct body and not in just one of the potentially many initializers.

5 Likes
Terms of Service

Privacy Policy

Cookie Policy