Improving the UI of generics

The runtime support I'm implementing is being done with an eye toward supporting generalized opaque types in the future, so if all goes well, there won't be further runtime deployment constraints on generalizing the feature (except maybe for debugger and tooling support).


You are correct. If the values involved are immutable, then it wouldn't formally be possible for the dynamic type of first to change from the element type of x. This ties in with the discussion of "opening" existential types a little further down. Right now, Swift's type system doesn't have any way to tie back into the dynamic types of values, so if x is any Collection then the best type we could use to describe the type of x.first! would be Any. For immutable values, we could implicitly open existential types, letting us say instead that first has the type type(of: x).Element or something like that. That would then let you use the value as an Element of x. This would however fundamentally only work with immutable values.

1 Like

First of all thanks @Joe_Groff for writing this up! It definitely clarifies some concepts around Swift type system that we keep trowing around on these forums but that are not easy to grasp. I'm pretty sure I'm still a little confused so don't mind these questions.

  • Would it make sense to keep these together with the manifesto in the repo? This information is quite useful and it would be nice to have it in a more accessible place (maybe even in official documentation or the book?)

Do these changes mean that any normal use of protocols would require the extra any keyword? I'm imagining all the DI uses of protocols and I'm not sure it's good marketing for Swift.

protocol NetworkManager {...}
class VM {
 init(networkManager: any NetworkManager) {...}
}

If that's the case, even if I like the direction of this, I'm a little hesitant. If is not the case it mens I need to read it again ^^'

I think on this line I agree with other comments in that it seems to extend all implementors of Hashable. Maybe is just because I don't fully get it or because I"m not used to it.

This is something that has been commented a bunch but I'm pretty sure that the majority of Swift devs wouldn't understand that easily. For my own sake, can somebody explain why that is the case? And I think it would be worth adding that to the original text.

Even with these concerns and not being an expert on the topic I'm really excited to see the future direction of the type system in Swift. I'm a huge fan of having this bigger overarching manifestos.

Thanks!

By a "normal" use of protocols, do you mean use of protocols as types? That's what I'm suggesting, yeah. If we look back at ObjC, this would be similar to how using a protocol as a type required writing id<Protocol>.

4 Likes

Yes that's what I meant. Thanks for the quick reply.

Looking back at Objective-C it doesn't look that bad ^^' I still think it will have a little of bad reception, specially from people coming from the mentioned languages with "interfaces", but if I prefer to think in the future :smiley:

Cheers

How would this relate to multi-parameter protocols? If we use “generic protocol” syntax in this way is there some other syntax that we might use for multi-parameter protocols? What might that look like? I would hate to see multi-parameter protocols be precluded just because people have a hard time with associated types mostly due to current limitations in the type system that should be lifted.

This is a fantastic idea! I missed it earlier. It would further reduce the cases where angle brackets are necessary.

Doesn’t the syntax of the previous quote make this unnecessary? With that sugar we can write it like this:

func foo(a: some Collection A, b: some Collection B) -> some Collection Result
  where A.Element == B.Element, B.Element == Result.Element

We can also require the input collections have the same type without having to use angle brackets:

func foo(a: some Collection C, b: C) -> some Collection Result
  where C.Element == Result.Element

This is nice! Would we still support extending the protocol directly for cases when you wanted to extend both the existential and the conforming types?

1 Like

I personally don't think that using the "generic protocols" for multi-parameter protocols is a good idea, since IMO the generic notation is the "good syntax", but multi-parameter protocols without a functional dependency are not the common case. Furthermore, the generics syntax implies an asymmetric relationship between Self and the arguments, which is appropriate for associated types, but completely artificial for multi-parameter protocols. I like the idea of a tuple-ish syntax like early versions of Rust had:

protocol (A, B): Protocol { }

func foo<A, B>() where (A, B): Protocol

or an application-like syntax similar to what's been proposed for Go 2:

protocol Protocol(A, B) { }

func foo<A, B>() where Protocol(A, B)

Since the set of protocols that need to be modeled this way is the minority, I think it's OK for it to have a somewhat more obscure syntax, if we ever support it at all.

4 Likes

This makes sense and the syntax above looks reasonable to me. I do hope we support this eventually. It would be nice to be able to model things like conversions, etc. Regardless, with other reasonable options for multi-parameter protocols available, using the generic syntax for associated types such as Element that would commonly be bound in existentials makes sense to me. This makes even more sense if it allows us to say any Collection<Int> (as so many people have requested) instead of any Collection<.Element == Int>.

1 Like

This is very good point. Could using <> after the type name be combined with existing generic parameter syntax (<> after the function name)?

func concatenate<T>(
    a: some Collection<Element == T>, 
    b: some Collection<Element == T>
) -> some Collection<Element == T> { }

Granted this example kind of breaks down if you need the two inputs to be the same collection type.

I also like this idea:

Could this be combined with the trailing <> syntax?

func concatenate(
    a: some Collection, 
    b: some Collection<Element == a.Element>
) -> some Collection<Element == a.Element> { }

Or perhaps where would be more readable.

func concatenate(
    a: some Collection, 
    b: some Collection where Element == a.Element
) -> some Collection where Element == a.Element { }

Perhaps this could be made more concise with an alias:

typealias CollectionOf<T> = Collection where Element == T
func concatenate(
    a: some Collection, 
    b: some CollectionOf<a.Element>
) -> some CollectionOf<a.Element> { }
1 Like

I would be really disappointed if we did that. Everyone is familiar with generic types and if we would introduce full support for type nesting it will be unavoidable to also introduce generic protocols. I don‘t buy the argument Joe said that generic protocols are the minority as they are not possible today there is nothing you can compare to. There are plenty of use cases I had where generic protocols would be the right choice and the alternative solution was not using PATs but a complete redesign to workaround these shortcomings. When Joe first pitched this Protocol<.Assoc == T> syntax I explicitly asked if it will interfere with the generic protocols and got as a reply that the leading dot is meant exactly for the use-case to refer to an associated type, which means if you had a generic protocol with associated type you could still write Protocol<Param, .Assoc == T> where Param is the generic type parameter of the generic protocol. That said I don‘t understand the resistance here nor do I appreciate the desire of trying to come up with some syntax for an alternate feature which eventually would block GPs.


I think as both Rust and Swift developer @regexident knows exactly the best practices on how to use PATs and GPs or even a mix of them that allows great expressiveness in the code base.

5 Likes

One more thing against any Collection<T> and in favor of any Collection<.Element == T> is that with the latter syntax we can express a super-type constraint as well any Collection<.Element: T> while the former syntax does not allow that.

4 Likes

I am also against an eventual generic-like syntax for primary associatedtypes: even though Swift doesn't have generic protocols and likely won't have them for a good while (if ever), the "concept" of them exists and even if not well known or understood it is fitting with the rest. Overriding that to make PATs nicer for the majority of use-cases but against more complex one I feel would be a bad move.

4 Likes

I like the idea and how it feels, but at the same time I have a reservation about it: having the type name after the constraint makes it feel like the name is attached to that, so in a way if this works I would feel that any Collection C should work as well, but it doesn't as C is really about some.
Maybe some<C> Collection? Though this basically goes down the road of having some<C: Collection> and any<Collection>, which works but I'm not sure that I like

What sorts of examples do you have? The most common examples I see from Rust are conversion-style protocols, a design choice Swift generally avoids in favor of promoting common currency types, and where the implied asymmetry of the generic syntax feels counterproductive to me. It isn't clear except by convention which end of the conversion should be Self versus being a generic parameter. Using a more application-style syntax could let us deploy argument labels to make the relationship between peer types in a conformance clearer.

The concept of multi-parameter type classes exists, yes, and Rust and C# choose to spell them as generic protocols, but that's not the only way to express them. In JVM languages, generic interfaces behave more like Swift associated types, so there's precedent for both behaviors.

2 Likes

Even if we make the syntax for naming type arguments more lightweight, and bring the declaration closer to the matching value's declaration, I think there's still value to not having to name things when the thing being named doesn't have much of an identity of its own. There's still cognitive overhead to having to juggle those names.

3 Likes

What you just said and extraction of the generic type that is not fixed on the conforming type. That last thing is a huge pain with current generics feature set. PATs provide only one fixed type on the conforming type while I have a generic and static API which requires a non-fixed generic type which PATs don't allow.

Can you give a more concrete example?

Not at the moment, as I would need to find that part of the API architecture that I wanted to design with GPs and abstract it so I can post it here, but that might take some time. I'll try to find it in the next couple days.


Rust explicitly communicates that in most use-cases the user likely want PATs, so does Swift already, but Rust does not abandon GPs right away as they are still useful, so why can't Swift do the same? Is it the implementation complexity for the compiler, or is it just a resistance to support that particular feature?

This is how I feel as well. The syntax Joe posted upthread models what is going on much better than generic syntax. This avoids the arbitrary choice of who gets to be Self which would always be a frustrating and annoying choice to have to make.

Absolutely! I’m not suggesting otherwise, only that it would be cool to be able to lighten up the syntax required when we do need the names and that it avoids the need for type(of:) and return.

I still think the Protocol<.AssociatedType == T> syntax is desirable. It makes the most sense for existentials and if we do it there we should be consistent and also support it in opaque types.

1 Like

I'm not suggesting we abandon the functionality, I'm saying that I don't (personally!) think that "generic protocols" are the best way of exposing multi-parameter protocol functionality.

2 Likes

If we go in this direction maybe it helps solve the problem of nesting protocols in types. Generic parameters of the type would become an associated type in the protocol. Uses of the protocol in the body of the type would have an implied same-type constraint:

class MyViewController<Model>: UIViewController {
    protocol Delegate { ... }
    var delegate: Delegate? // Delegate<.Model == Model>?
    
    protocol P { ... }
    // implied where T.Model == Model constraint
    func doSomething<T: P>(with value: T) { ... }
}