[swift-evolution] Proposal: helpers for initializing properties of same name as parameters
Matthew Johnson
matthew at anandabits.com
Sat Dec 5 23:31:50 CST 2015
Thank you for taking the time to provide this feedback!
I was trying to focus more on the problem we need to solve and just quickly sketch something that would solve it by building on the previous proposal. I agree the syntax (and likely semantics) left a lot to be desired and should be vastly improved. I was primarily demonstrating that is *possible* to solve the problem, we just need to find the right solution.
I’m glad to hear you also believe it’s a problem worth solving and just didn’t like specific details of the solution. I do think it’s worth solving the more general problem even though a good solution may be more difficult to find. If we adopt a simpler solution like the one you propose for default initializers it is likely to significantly constrain our options for solving the more general problem (in order to play nice with the simpler solution you propose).
I also believe that this problem is intimately related to the problem addressed in the “setup closures” thread as a general solution would supersede the need for “setup closures".
I would like to try coming at this problem from a different angle and see if that solution feels any better to us. Let’s consider an @initializable attribute for members:
@initializable members that are set by the initializer are not generated as parameters by the compiler as the body of the initializer is taking responsibility for initializing that member. The initializer is free to manually declare the a parameter with the name of the member it is initializing and use that if desired but it is not required to do so. This will ensure boilerplate still works as expected when a programmer provides it.
@initializable members not set in the implementation of an initializer are generated as parameters automatically by the compiler at the end of the argument list (but before any variadic or trailing closure arguments) in the order the members are declared in the type itself. If the member declares a default value, the compiler uses that as the default value for the argument. Ideally the caller can re-order the labeled arguments as desired.
Private members are allowed to be @initializable and may be exposed through internal or public initializers. The member itself is still hidden outside the scope of the file but any internal or public initializers should still be able to accept an initial value from a caller if desired, just as is possible with manual boilerplate today.
The general form of an initializer with automatically generated parameters would look something like this:
init() {
// Compiler generated code to set the members for which it generated parameters.
// The initializer is now free to access the value proved by the caller by accessing the member which has now been initialized by the compiler generated code.
// The initializer sets any members it needs to directly.
// This includes all members not marked @initializable that do not have default values.
}
There are probably some additional details to work out in relative to two-phase initialization of classes, but I think this conveys the basic idea.
Here is an example:
struct S {
@initializable let i: Int
@initializable private let s: String = “s”
let f: Float = 1
let d: Double
// i must be provided by the caller
// s may be provided by the caller but is not required as there is a default value of “s”
// f may be initialized directly but does not need to be as there is a default value of 1
// d must be initialized directly by the initializer
// callers see init(i: Int, s: String = “s")
init() {
// the initializer is free to read both i and s as the compiler automatically initializes them
// with the value provided by the caller or the default value before the body of the initializer is entered
d = Double(i)
// f is initialized by the compiler to its default value
}
// s may be provided by the caller but is not required as there is a default value of “s”
// callers see init(i: Int, j: Int, d: Double, s: String = “s")
init(i: Int, j: Int, d: Double) {
// the initializer is free to read s as the compiler automatically initializes it automatically
// with the value provided by the caller or the default value before the body of the initializer is entered
// this initializer may not read the value of i because the compiler does not automatically generate
// an initializer for it as is initialized directly
// the i parameter does not need to be used to directly initialize self.i and can be used to perform computation
// if desired in order to provide backwards compatibility with existing code
self.i = i + j
self.d = d
// f is initialized by the compiler to its default value
}
}
One drawback to a solution like this is that it could result in the compiler generating parameters for an initializer that the developer did not intend but “forgot" to initialize directly.
When it does so for a member that does not contain a default value, tests and calling code should fail to compile calling attention to the problem. In this case the problem is not too severe.
On the other hand, if the member declares a default value calling code would still compile and the member would be initialized to the default value. This is identical to the behavior of member not marked @initializable that a developer “forgets” to initialize if it declares a default value (i.e. the correct behavior for the initializer would require a value different than the default). The primary difference is the compiler generated parameter for the @initializable member that the implementer “forgot” to initialize, potentially allowing a caller to provide a value and initialize the instance in an incorrect way that is *different* than the default value which should not have been used by the initializer.
Another drawback to this approach is that it is not explicitly clear when reading an initializer’s parameter list. One way to mitigate this would be to require the initializer itself to be marked with @initializable in order to opt-in to the compiler generated parameters. This at least alerts the reader that they exist. It provides less information than the "init default()” approach as some @initializable members might be initialized directly in the initializer body which would suppress the compiler generated parameter. However, the semantics of @initializable are pretty straightforward so I don’t think this would cause too much confusion.
Documentation generated for the initializer, autocomplete, etc would all expose the full parameter including the parameters the compiler generated for @initializable members.
I would like to revisit the Player example to see how it would fare using this approach:
class Player {
@initializable var name: String
@initializable private(set) var points: Int
func addPoints(additionalPoints: Int) {
points += additionalPoints
}
// the previous post stated ‘Creates init(name: String, score: Int)' which does not match the members
// this has been modified to match the members as was likely intended
// Creates init(name: String, points: Int)
init() {}
}
later in Player’s life:
class Player {
let syncIdentifier = NSUUID()
var name: String
private var pointChanges: Set<PointChange>
var points: Int {
get { return pointChanges.reduce(0, combine: +) }
}
func addPoints(additionalPoints: Int) {
pointChanges.insert(PointChange(offset: additionalPoints)
}
// We can no longer use the compiler generated parameters, but we can still create an initializer with the same signature.
// Furthermore, if we do not need to be concerned about the declaration order of keyword arguments
// we can now leave off the boilerplate for “name” and allow the compiler to generate it
// without requiring any changes to call sites.
init(name: String, points: Int) {
self.name = name
pointChanges = [ PointChange(offset: points) ] // the original post had score here which did not match the parameter name
}
}
Does this solution look any better to you than the previous idea?
> On Dec 5, 2015, at 4:31 PM, Brent Royal-Gordon <brent at architechies.com> wrote:
>
>> Did you read through by reply to the original proposal? I believe it provides the convenience you're looking for here while still allowing for the flexibility to avoid boilerplate in initializers that do more than just set properties.
>
> I did, and I didn’t really like it. One of the syntaxes it proposed was strange and ad-hoc; the other was a fairly arbitrary overloading of parentheses, although it doesn’t actually conflict with anything since you can’t overload “calling parentheses” on a non-function.
>
> It’s important to ensure that, when the implementation evolves, you can replace the initializer transparently. For instance, suppose I have a type which represents a player in a game. It uses a stored property called `points`:
>
> class Player {
> var name: String
> private(set) var points: Int
>
> func addPoints(additionalPoints: Int) {
> points += additionalPoints
> }
>
> // Creates init(name: String, score: Int)
> init default()
> }
>
> Later, I decide to add syncing, and realize I need to change this model. I need to add a sync identifier, and I want to change `points` to be computed from a series of `PointChange` objects. (This is a common strategy to avoid sync conflicts.) However, I don’t want to disrupt any of my existing code when I do this.
>
> class Player {
> let syncIdentifier = NSUUID()
> var name: String
> private var pointChanges: Set<PointChange>
>
> var points: Int {
> get { return pointChanges.reduce(0, combine: +) }
> }
>
> func addPoints(additionalPoints: Int) {
> pointChanges.insert(PointChange(offset: additionalPoints)
> }
>
> // We can no longer use init default(), but we can still create an initializer with the same signature
> init(name: String, points: Int) {
> self.name = name
> pointChanges = [ PointChange(offset: score) ]
> }
> }
>
> By *not* separating the properties into a different syntactical construct from the constructor parameters, I can update my type’s implementation without affecting its interface. If properties were separated syntactically from constructor parameters as you proposed, it would not be possible to change this seamlessly.
>
>> If you believe we should have a feature focused strictly on memberwise initializers and not also allow for less boilerplate in more sophisticated initializers I am interested to hear your rationale.
>
> Honestly, I would like to fix the boilerplate issue in more sophisticated initializers, but I don’t see a clean way to do that. The other proposals I’ve seen in this thread, along the lines of:
>
> init(self.name, self.points) {}
>
> Frankly do too much violence to the parameter list. Swift’s declarations have a lovely feature where the declaration basically looks like the usage, except that the concrete values in the usage are replaced by types in the declaration. These proposals destroy that property—when I look at the declaration, I can no longer envision what the call site will look like.
>
> Basically, I focused on default initializers because they’re *really* easy to solve and cover a good portion of the common case. I really have no idea how to solve the larger problem, and I haven’t liked any of the other ideas in this thread, but I do know how to solve this more limited problem.
>
> --
> Brent Royal-Gordon
> Architechies
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151205/01673358/attachment.html>
More information about the swift-evolution
mailing list