Protocol metatype extensions (to better support SwiftUI)

Swift's dot shorthand for static factories is wonderful! Unfortunately, it currently requires a concrete type context in order to work. A while ago I pitched an idea for lifting this limitation. It turned out that the original idea in that pitch isn't viable, but the thread did lead to some interesting discussion that pointed in a potentially more viable direction.

SwiftUI

SwiftUI is using the workaround I described in the form of StaticMember. Desipite having developed the workaround and used it in a few projects, it took some head scratching before I recognized the role StaticMember was playing in SwiftUI. Quoting the documentation:

Use StaticMember to support implicit member expressions, sometimes referred to as leading dot syntax, where they would normally not be supported, like static properties representing concrete values provided to a protocol-constrained generic function. For example:

protocol ColorStyle {
    typealias Member = StaticMember<Self>
}

extension StaticMember where Base : ColorStyle {
    static var red: RedStyle.Member { return .init(.init()) }
    static var blue: BlueStyle.Member { return .init(.init()) }
}

extension View {
    func colorStyle<S : ColorStyle>(_ style: S.Member) -> some View {
        ...
    }
}

MyView().colorStyle(.red)
MyView().colorStyle(.blue)

As an Apple framework that will live a very long life, I don't think SwiftUI should be introducing a workaround like this. I think we should revisit this topic and introduce proper langauge support if at all possible. If one isn't possible we should consider moving StaticMember into the standard library as a standard workaround everyone in the community can become familiar with and share.

Previous discussions and big picture

As I mentioned, the earlier thread on this topic pointed in some interesting directions and I continued to think about it. This thinking lead to a broad sketch for a direction in which we are able to extend metatypes and existentials in additiona to concrete types. Discussion of that sketch led to a design that uses the meta keyword for extending metatypes. (It also builds on the any and some keywords that are a part of Improving the UI of generics)

At a high level, this design sketched at the above links enables the following forms of extensions (among others):

  • extension P contionues to extend both conforming types and the existential as it does today
  • extension any P extends a protocol existential only (not conforming types or the metatype)
  • extension some P extends conforming types onyl (not the existential or the metatype)
  • extension meta P extends the protocol metatype only (not the existentia or conforming types)

Proposed solution

I propose that we shoud introduce protocol metatype extensions and use those to drive dot shorthand in generic contexts. The above SwiftUI code would be streamlined as follows:

protocol ColorStyle {}

extension meta ColorStyle {
    static var red: RedStyle { return .init() }
    static var blue: BlueStyle { return .init() }
}

extension View {
    func colorStyle<S : ColorStyle>(_ style: S) -> some View {
        ...
    }
}

MyView().colorStyle(.red)
MyView().colorStyle(.blue)

// Static members of the protoco metatype may be accessed explicitly as well
let myStyle = ColorStyle.red

// Conforming values stored in variables are directly compatible with the generic API
// which is not the case with APIs based on a `StaticMember` wrapper type
MyView().colorStyle(myStyle)

In this design, dot shorthand will consider all static members of the protocol metatype which return a value of a conforming type. This removes the need for a wrapper type and streamlines the API of the library. It also improves support for passing variables of conforming types directly to an API.

Without direct language support for this shorthand a library author is left with two bad choices if it wishes to support both dot shorthand and direct values. One choice is to require users to wrap values of conforming types. The other is to provide two overloads, one taking a conforming value and the other using a wrapper to drive the use of dot shorthand. The first option forces additional syntax on users of the library. The latter introduces a lot of boilerplate into the library and creates potential for ambiguity. Both options may cause confusion for users of the library who may not understand the design.

In summary, let's bridge the gap between dot shorthand and generics so these language features are more directly compatibe with each other. (And let's do it before SwiftUI ships with a workaround)

31 Likes

I'm all for brining meta into the language and cleaning up the metatypes part of the language, in a similar way we discussed in the linked thread about generics UI. If that gives us even more expressiveness for free, even better.

1 Like

I'll admit I was a little baffled yesterday by StaticMember when looking at the SwiftUI API. In my mind, if something needs language hacks to work, that reflects poorly on the language or the API. Even the documentation page trying to explain StaticMember makes it look like an obscure magic incantation.

I think I get how StaticMember works now, but learning this feels similar to learning std::enable_if in C++. In both cases I can recognize the cleverness and the usefulness, but it's an absolute disaster for understandability of the APIs it touches.

If we're serious about this, it should be made a feature of the language.

15 Likes

Why @John_McCall, @Douglas_Gregor or @Joe_Groff are not reacting to this?
Do StaticMember are internally assumed that they will be part of SwiftUI framework and out of the scope of something that should be addressed for Swift 5.1?
Or is it something worth addressing post swift 5.1?

1 Like

I hate to bump my own thread, but I would really appreciate it if someone from Apple could comment on this pitch. Is this something that might be viable or should I move ahead with a proposal to move StaticMember into the standard library? I really don't think StaticMember belongs in a UI framework.

5 Likes

If nothing else you alleviated my confusion with StaticMember. I am following this pitch for sure.

Could you provide a sample of the difference between extending the metatype and extending the existential with a static property?

extension meta ColorStyle {
    static var red: RedStyle { .init() }
}

vs.

extension any ColorStyle {
    static var red: RedStyle { .init() }
}

It's sort of unclear to me why the static keyword would be needed in the first case. Are these equivalent (and if so, do we need both)? Or would the latter be disallowed maybe?

1 Like

If the first extension had static then you're creating an instance member on the metatypes metatype.

protocol ColorStyle {
  metatype Protocol {
    metatype Type {
      var red: RedStyle { get }
    }
  }
}

You can access the value though ColorStyle.Protocol.self.red.


The second extension adds the property to the metatype of any type that conforms to ColorStyle.

protocol ColorStyle {}

struct S: ColorStyle {
  metatype Type {
     var red: RedStyle { get }
  }
}

You can access the value through S.self.red.


In short there is a difference where the extension will live as an instance member, if you add or omit static it will alter the final destination.

extension meta T {}
// is the same as for non-protocol types
extension S.Type {} 
// or like this for protocols
extension P.Protocol {}

The good thing about meta T is that it would hide the difference between .Type and .Protocol.


Please note that metatype keyword does not exist in Swift nor is the above valid Swift code, but you can imagine that Swift is creating some metatypes like this and simply nest them.

1 Like

@anandabits you may need to re-check your example in the proposed solution. The usage of static always nests the member further down into a metatype implicitly.

Is far as I'm concerned, the following code sample won't work with the extension from original post:

extension View {
    func colorStyle<S : ColorStyle>(_ style: S) -> some View {
        ...
    }
}

MyView().colorStyle(.red)
MyView().colorStyle(.blue)

For that to work you need an extension that looks more like this:

extension ColorStyle {
  static var red: RedStyle { return .init() }
  static var blue: BlueStyle { return .init() }
}

// or
extension<T: ColorStyle> meta T {
  var red: RedStyle { return .init() }
  var blue: BlueStyle { return .init() }
}

// or
extension<T: ColorStyle> T {
  static var red: RedStyle { return .init() }
  static var blue: BlueStyle { return .init() }
}

Well, I guess this is my question, because the example in the OP (which I just copied in my post) uses static, but the member is on the metatype, not the metatype's metatype.

I think this is a mistake in the example in the OP.

extension meta Protocol {
  var property: SomeType { ... }
}

Is the correct way it should have been written.

Yes, this is correct. @DevAndArtist and I discussed this in Slack and I believe with this correction he is on board with the design. @woolsweater does this correction answer your questions as well?

I believe it does, thanks!

Edit: Oh, but does that mean static is disallowed in an any extension?

No, if P is a protocol then any P is the protocol as type, or also known as an existential. Therefore if you extend any P with static member it is equivalent if you would extend meta any P with instance members, or in other words you‘re extending the metatype of the existential type of P. Remember that every type has a metatype, even the metatype itself (which is theoretically an infinite recursion).

extension any P { static var foo: Int { return 42 } }

extension meta (any P) {
  var foo: Int { return 42 }
}

And one thing that needs to be mentioned. If we would introduce meta keyword, there will be also any meta T which is an existential of the metatype. This behavior is not clear at the first glance but it would mimic the current type relationships of .Type metatypes.

To improve readability we could offer a type alias:

typealias Meta<T> = meta T

Then something like any Meta<any P> is more readable then any meta any P.