Yes, I concur! We'll work on updating the documentation.
Should the following work?
extension ToggleStyle where Self == CustomToggleStyle {
init(custom: Int) {
/// An initializer defined on CustomToggleStyle
self.init(...)
}
}
let toggleStyle = ToggleStyle(custom: 42)
This proposal is scoped to leading dot syntax only, behavior of other code would stay the same.
At first glance, this proposal seems like the wrong way to achieve its goal, and an abuse of the type system. Now that we have property wrappers in function arguments (or is that still pending?) I think the right way to achieve this would be for SwiftUI functions to take an enum that they automatically transform into an instance of the correct type. It would look something like this:
enum ToggleStyleEnum {
case `default`
case `switch`
case checkbox
case other(CustomToggleStyle)
}
@propertyWrapper
struct ToggleStyleWrapper {
init(_ ts: ToggleStyleEnum) {
switch ts {
...
}
}
}
function toggleStyle(@ToggleStyleWrapper toggleStyle: ToggleStyleEnum) {
...
}
It's interesting, because I view the solution from this proposal as a natural extension of existing lookup rules, and the property-wrapper-based solution as an abuse of the type system!
I don't think we should encourage the pattern of using argument wrappers as implicit arbitrary-type-conversion operators at function argument boundaries.
I still really want extensions on the meta type, but as long as this doesn't block that from being added in the future, this seems like a fine addition.
I'll still complain that these declarations are overly complicated and their intended usage is hard to decipher:
extension ToggleStyle where Self == DefaultToggleStyle {
public static var `default`: Self { .init() }
}
extension ToggleStyle where Self == SwitchToggleStyle {
public static var `switch`: Self { .init() }
}
extension ToggleStyle where Self == CheckboxToggleStyle {
public static var checkbox: Self { .init() }
}
The language would express the intent better using a new keyword:
extension ToggleStyle {
public factory var `default`: DefaultToggleStyle { .init() }
public factory var `switch`: SwitchToggleStyle { .init() }
public factory var checkbox: CheckboxToggleStyle { .init() }
}
I think I'd be fine if this was just sugar over the current proposal. It'd be unfortunate however if it was introduced first without the sugar since that's how people will start teaching the feature to each other.
This is what I was going for with my enum + function builder example, but 100x better.
I like that notation, but don't think it should be sugar for the current proposal.
It would work well as a more intuitive notation for placing functions on the meta type. I would also love to see factory init
s that allow you to build and return any subtype of the protocol!
What would happen here? Would we disallow re-declaring foo
or complain that .foo
is ambiguous?
extension ToggleStyles where Self == SwitchToggleStyle {
static func foo: Self { .init() }
}
extension ToggleStyles where Self == CheckboxToggleStyle {
static func foo: Self { .init() }
}
Toggle("Wi-Fi", isOn: $isWiFiEnabled)
.toggleStyle(.foo)
That ought to raise an ambiguity error, since there's no single type to infer from the member. That's what would happen if you did the same thing with constrained extensions on a generic type.
@Joe_Groff This was merged 6 days ago so it's approved right?
https://github.com/apple/swift/pull/34523
Yes, the core team has accepted this proposal, with the revision suggested by John at the start of the thread.
Thought I'd drop a quick note to highlight a possible failure case for this proposal's implementation, described by [SR-15853] Re: SE-0299: Static member lookup fails in type that has two tuples. · Issue #58127 · apple/swift · GitHub.
IIUC the code you've posted in that bug doesn't introduce the proper generic context for the static members to be resolved under SE-0299, since Model.a.y
and Model.b.y
are of existential type Tint
rather than generic type constrained to Tint
. Changing the definition of Model
to the following allows this to type check:
struct Model<T: Tint> {
let a: (x: String, y: T)?
let b: (x: String, y: T)?
}
ETA: though this still appears to be a bug with implicit member syntax in general since this works with the non-tuple and single-tuple versions. I just don't think it's related to SE-0299 specifically.
Thanks, that makes some sense. Though with that said, if not under SE-0299's implementation, perhaps you can clarify for me how the swift compiler resolves the member type of the existential, then? Note that the requirements on how the members should be declared on the protocol extension are exactly in line with SE-0299.
Actually, after looking into it a bit further, I take it back—I think this is related to SE-0299. This sort of inference through an existential was not permitted prior. I was confusing it with the behavior that allowed the result of a chain to be a subtype of the overall result type for subclasses.
cc @xedin is the lookup behavior in an existential context intended here?
SR-15853 looks like a bug to me which was been fixed recently, I have tried with 02/03 snapshot of main and the example type-checks correctly with that toolchain.
Ah, I didn't realize that SE-0299 was also intended to work in existential contexts—I thought it was restricted only to generic contexts. Perhaps the proposal title/body should be adjusted to make it clear that this use case is supported?
I understand what you mean although it took me a bit :) Strictly speaking it's indeed not to the letter of the proposal since it would infer a base type not based the generic conformance requirement of the result, but directly from the protocol of the existential type, so maybe it indeed makes sense to document that somehow...