Why can't a variadic generic initailizer (that allows zero arguments) count for a protocol's default initializer requirement?

Continuing the discussion from How could someone delegate to a generic-style variadic initializer?:

In other words, shouldn't

  init<each S>(_ somethings: repeat each S) { /*...*/ }

be able to count as implementing init()? (This is like when we allowed enum type cases to pose for a type-level method requirement.) Is this a bug? Or is there something subtle type theory where allowing this makes a program go to :poop: ?

There are a 64 open "go to :poop:" issues with parameter packs on the main repo right now... search:

https://github.com/swiftlang/swift/issues?q=is%3Aissue%20state%3Aopen%20crash%20(%22parameter%20packs%22%20OR%20%22variadic%20generic%22)

Probably need to fix the crashes before expanding the footprint.

1 Like

The behavior you're seeing isn't specific to parameter packs. You could make an argument that this should be allowed as well:

protocol P {
  init()
}

struct S: P {
  init(x: Int = 0) { /* ... */ }
}

The way I understand it, protocol witness matching is based on how a member is declared, not how it can look when it's called.

Could the compiler synthesize a thunk here to use as the protocol witness when there is a compatible but not exact match? Perhaps, but that would need to go through Swift Evolution and would need to make sure it doesn't cause issues for type checker complexity.

1 Like

I'd say this is a separable issue, given the design choice that the default value is emitted in the caller with its implications for ABI, etc. One can legitimately argue that there isn't a 0-ary initializer in your example, since any trampoline you could write would have that default value in the callee, which is subtly different.

That's not the case for the parent: a variadic initializer really does mean we have a 0-ary initializer with no caveats. I think the answer here is more mundane: it can, it just doesn't--it simply hasn't been implemented (and would require a proposal to do so).

1 Like

This feels like a distinction without a difference in terms of what a user, not a compiler engineer, would expect. The mismatch is exacerbated by the fact that there's no way for the init() that the conformer has to write to delegate to the init(x: Int) without explicitly restating the default value to disambiguate the overload that they want, which ends up emitting the default value into something akin to the callee anyway.

It's a subtle but user-visible difference on an ABI-stable platform:

public protocol P {
  var x: Int { get }
  init()
}

// ABI-stable library, version 1
public struct S: P, Equatable {
  public var x: Int
  public init(x: Int = 42) { self.x = x }

  // Hypothetical, or what would be thunked (presumably doesn't compile today):
  @_implements(P, init())
  public static P_init_witness() -> Self { init() }
}

// ABI-stable library, version 2
public struct S: P, Equatable {
  public var x: Int
  public init(x: Int = 21) { self.x = x }
  // ... all else same
}

// User's app
func f<T: P>() {
  let s = S()
  let t = T()
  print(s.x == t.x) // `true` if recompiled; `false` otherwise
}

(Of course, in present-day Swift, there are non-hypothetical ways of coaxing out this difference; but we're talking here about the specific claim regarding whether we can/can't extend what counts for a 0-ary initializer requirement's implementation, so to go with that I'm sticking with the hypothetical.)

My point here only is that there's really no such wrinkles with parameter packs--we simply don't have a proposal or implementation.

1 Like