SE-0242: Synthesize default values for the memberwise initializer

Wow. I'm very unhappy with our current behavior then, and I agree with Matthew that it shouldn't block this proposal.

4 Likes

Perhaps this is the motivation we need to revisit SE-0018 and the initializer model more broadly. If you’re interested in collaborating as an implementer let me know and maybe we could work on a proposal together! :wink:

4 Likes

Agree.

1 Like

Of course. Explicitly declaring an argument with a default value using the memberwise initializer does not evaluate the default initializer like @michelf 's example shows.

func zero() -> Int {
  print("Can I have the number 0 with a side of side effects?")
  return 0
} 

struct X {
  var y = zero()
}

// Explicitly assign the y: argument in the memberwise
let x = X(y: 1) // nothing is printed
2 Likes

Which seems perfectly reasonable when I look at it from perspective where the same syntax is used to initialize a let which in my mental model can be assigned to exactly once.

I got quite dizzy from all the discussion about double executing functions in initialization of struct… observable side effects here are what will get you into the trick-interview-question territory. That’s just plain wrong!

The point of initialization is to ensure that all fields have a valid value after it’s done. Any observabale side-effects that show anything more than a single assignment are dangerous leaks of internal implementation details that should be considered a bug.

Swift’s value types are the key aspect borrowed from functional programming style which greatly simplifies the user’s mental model. This proposal is about extending the ability to use the sweetest and shortest syntax in a presence of default values and is AFAICT fully consistent with how the generated memberwise initializer behaves now.

1 Like

The proposal says "This is a purely additive feature, thus source compatibility is not affected.", which I think is not true, because an overload of .init is added in some cases.

Here is an example of code that used to work, but wouldn't anymore.

struct A {
    var b: Int = 0
}

let c = A.init //Now is ambiguous between init() and init(b:)

Either way, -1 to the proposal for clarity reasons, I dislike memberwise initializers already anyways, I don't want even more. I favor explicitness over convenience in most cases.

There's not an ambiguity as this compiles just fine today:

struct A {
    var b: Int
    
    init(b: Int = 10) {
        self.b = b
    }
}

let c = A.init
1 Like

The analogy would be:

struct A {
    var b: Int = 10
    
    init() {}
    init(b: Int = 10) {
        self.b = b
    }
}

let c = A.init

This appears to work and picks the default initializer.

I see, you are right, it seems like the proposal would just add default arguments to the one sole memberwise initializer.

I do actually think that it should be considered ambiguous though.

For example, this should compile:

protocol A {
    func b()
}

struct C: A {
    func b(d: Int = 0) { }
}

That doesn't require any ambiguity, it just requires the compiler to synthesize a b() for the A conformance that calls b(d:) with the default value. Which is another, more complicated, proposal.

Okay, but ignoring protocol conformance, I want these pieces of code to be treated as truly equal:

struct A {
    func b(c: Int = 0) { }
}

And:

struct A {
    func b(c: Int) { }

    func b() {
        b(c: 0)
    }
}

And then the init example would become ambiguous too

We're getting off topic a bit, but why would we want to introduce an ambiguity?

1 Like

The ambiguity is just a side effect of the real goal: For functions with default parameters to generate real overloads that fully participate in type inference. Just synthesizing these for protocol conformance would only solve a part of the problem and would be inconsequent. For example, this should be possible:

func a(b: Int = 0) { } //creates the overloads a(b:) and a()

func c(d: () -> ()) { }

c(d: a) //correctly uses a().

In a real world scenario, c could be something like Sequence.map.

Implementing it this way would automatically make it work with protocols too. This fixes a superset of the protocol conformance issue and I think it's clearly superior and more straightforward.

But you are correct, this is off topic here.

Proposal Accepted

SE-0242 is accepted, with a request that the original proposal be amended for clarification of actual behavior. The Core Team felt that the proposal should more explicitly illustrate the actual behavior in a few specific cases, as this was the crux of some of the back-and-forth in the review thread.

Note: the proposal has been updated to include more details in the “proposed solution” section, as requested by the Core Team.

There was some concern in the review thread about potentially ambiguities, particularly with code looking like this:

struct A {
  var b: Int = 0
}

let c = A.init // Ambiguous between init() and init(b:)

The Core Team discussed this, and noted that there was already a more fundamental problem with ambiguity in which you can leave off the arguments when you use a function reference. This causes problems in many places today, and is something that should be explored further in a different proposal. This proposal doesn't seriously exacerbate the current problem.

The proposal review also discussed an interesting behavioral quirk with initial values and side-effects, for example:

struct Foo {
  var i: Int = getDefaultValue()
}

struct Bar {
  var i: Int = getDefaultValue()
  init(i: Int) { self.i = i }
}

let x = Foo(i: 1)  // No side effects
let y = Bar(i: 1)  // Yes side effects

The Core Team felt that this was a bug in the compiler's implementation of initializers. The failure here is that Bar calls the side effect, and it is not the right behavior. Addressing this would likely require an evolution proposal, or at least an assessment of whether or not fixing this would break existing code.

18 Likes

Is there a bug report filed for this yet?

I made one here: https://bugs.swift.org/browse/SR-10092

1 Like

Forum post here: User defined initializers always call member initializers

I strongly disagree that this is a bug. Initial value expressions should not have side effects, ideally, and if they do, it is easier to reason about them if we just say that they are always executed at the top of the initializer, than if where and when they're executed depends on the DI analysis.

To put it another way, initial value expressions are simple syntax sugar that you can eliminate by writing out the initialization by hand in each initializer; if your initial value expressions have side effects or don't always need to run for other reasons, it is best to avoid them altogether and write out the initializer body explicitly.
y

1 Like

How is this handled for let members? If a stored let property has a default value and it can be set in an initializer, then it's set only once, right? var properties could be set to use a similar rule.

let properties that have default values cannot be reassigned in an initializer.

struct Test { 
   let id = 10 
   init(id: Int) { 
       self.id = id 
   } 
 } 
error: repl.swift:8:17: error: immutable value 'self.id' may only be initialized once
        self.id = id
3 Likes