SE-0242: Synthesize default values for the memberwise initializer

You’re absolutely right; I forgot about the function reference case. Yeah, it makes sense to keep both then.

The discussion thus far has been focused on what happens when the default argument is used. The example you posted does not use the default argument, instead providing an explicit argument. @Alejandro can you describe the behavior exhibited by your implementation when an explicit argument is provided?

Yes, there are definitely ways to do this with a broader revisit of SE-0018 and the initializer model. I have some ideas as well. But as you note, they aren’t really relevant to evaluating the current proposal. For now, the important thing is to avoid specifying behavior that would make it harder to explore them in the future.

I think @michelf’s point is that the behavior of the synthesized memberwise initializer is already different from that of a manually-defined initializer:

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
6 Likes

Ahh, I missed that. Thanks for pointing that out. I wasn’t aware that this is how the current memberwise initializer behaves.

This means that the current memberwise initializer is already unutterable in exactly the way that @xwu is unhappy with and this proposal simply does not change that. It only adds default arguments to the parameters. IMO, that settles the matter.

11 Likes

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: [SR-10092] Custom initializers always call member initializers · Issue #52494 · apple/swift · GitHub

1 Like