Make dot shorthand work with generics and existentials


(Matthew Johnson) #1

Swift's dot shorthand for static factories is wonderful! Unfortunately, it currently requires a concrete type context in order to work. It is possible to mimic these forms of shorthand using a wrapper:

struct Sugar<T> {
    let value: T
}
extension Sugar where T == S {
    static var s: Sugar<S> { return .init(value: S()) }
}
func foo<T: P>(_ t: Sugar<T>) {
   // use t.value (and maybe T.self)
}
foo(.s)

struct MetaSugar<T> {}
extension MetaSugar where T == S {
    static var s: MetaSugar<S> { return .init() }
}
func bar<T: P>(_ t: MetaSugar<T>) {
    // use T.self
}
bar(.s)

This approach works in some use cases, such as DSLs where all arguments are expected to be created inline using the dot shorthand. However, this approach is very suboptimal when users need to pass values directly (they would need to wrap the value before passing it).

For this reason, a library wishing to provide this kind of syntactic sugar will likely resort to providing two overloads for each API (one to support dot shorthand and another for directly passed values that come from call-site variables). Unfortunately, this leads to a lot of boilerplate and only works for APIs shipped with a library, not extension APIs users provide themselves (unless users can be convinced to provide the boilerplate as well).

The solution is to lift the limitation and directly support dot shorthand in generic and existential contexts so code such as the following works:

protocol P {}
struct S: P {
    static let s = S()
}
func foo(_ p: P) {}
func bar<T: P>(_ t: T) {}

foo(.s)
bar(.s)

In generic contexts, the compiler would infer the type T based on the factory that was used. If two types meeting the constraints provided identical factories an ambiguity error would be produced.

This variant of dot shorthand could also support cases where a metatype is required. This can make it much more convenient to pass metatypes when they may be much more verbose than necessary given a particular constrained context.

extension S {
    static var s: S.Type { return S.self }
}
func bar<T: P>(_ t: T.Type) {}
bar(.s) // instead of `bar(S.self)`

In the above example, imagine that S has a very verbose type name, but P determines a context where a very short name is sufficiently clear. It might be possible to use a typealias to shorten the type name, but a typealias would need to live at global scope to be concise enough for this purpose.

Finally, specifying type names like Int.self in a call aiming for DSL-like syntax is not nearly as elegant as the dot shorthand form .int. (note: the desire to support the latter in an API is what led to my discovery that the "sugar" wrapper workaround exists)


(Jordan Rose) #2

How does the compiler know where to look for s?

What happens if there are two such types?

What if the expression is more complicated? Do all possible conforming members go in an overload set?

I'll stop using the question format and say this is basically untenable; it's the same lookup used for AnyObject but with extra behavior, and we've seen no shortage of problems from that (one of them being much worse incremental build times). I don't think we can implement this in any reasonable way without a link from the protocol to the concrete type(s) to look in.


(Xiaodi Wu) #3

This seems brittle. The addition of any static member to a conforming type could then be potentially source breaking, no? In fact, given the possibility of retroactive conformances, the addition of any static member to any type could potentially break this feature for some user, could it not?


(Matthew Johnson) #4

My thought was that it would look in conforming types although I would be open to any other approach to supporting this kind of shorthand.

It would look in both types. As noted in the pitch, it would produce an ambiguity error if there was not a single best match.

More complicated how so? It could be a dot shorthand to a factory function taking parameters, but if dot shorthand supports anything fancier than that I’m not aware of it.

Yes, all possible conforming members (i.e. static members of conforming types that return Self) go into an overload set. An alternative would be to locate this overload set independently to make the set visible to generic dot shorthand explicit. I’m not sure exactly what a design for that would look like but it is a direction that could be explored.

What would such a link look like? I don’t have a strong opinion on a solution. I’m just hoping it’s possible to do something to eliminate the need for the workaround.

It would be possible to introduce ambiguity when adding new members, yes. That’s not unique to this use case but I can see how it could be easier to do it unintentionally than in other contexts. Do you have any ideas on how we could expand dot shorthand to solve the motivating problem in a more controlled manner (such as the existing workaround, but without it’s drawbacks).


#5

I’m worried about the strain this could cause on compiler when there are a lot of ambiguous values, they do print a candidate list on other ambiguous context so I believe they would in this case as well. Ambiguous cases don’t seem that unlikely.


(John McCall) #6

Do you have an example of what you'd like that fails with generic types?

For protocol types, I think a better solution would be to allow some sort of static extension to the protocol which is understood to be adding members to the protocol type itself rather than the conforming types.


(Matthew Johnson) #7

This most recently came up during the discussion of fold and Monoid, but I have wanted something like this for quite a while and had quite a few use cases.

static extension sounds like a good solution!
It would provide a DSL-like syntax:

extension Sequence {
   func fold<M: Monoid>(_ m: M) -> M.Value where M.Value == Element { ... }
}
struct Sum: Monoid {... }
static extension Monoid {
    static var sum: Sum { ... }
}
mySequence.fold(.sum)

There are all kinds of places where this could arise, but it is especially useful in making ML-ish protocols more convenient to work with in Swift.

There are some places where I like to just pass the metatype and use empty enums for the conforming types (similar to ML structures). That could be supported with similar sugar using syntax like this:

extension Sequence {
   func fold<M: Monoid>(_ m: M.Type) -> M.Value where M.Value == Element { ... }
}
struct Sum: Monoid {... }
static extension Monoid {
    static var sum: Sum.Type { return Sum.self }
}
mySequence.fold(.sum)

@jrose @xwu @Lantua does this approach sit better with you?


(Jordan Rose) #8

:-) Yeah, this makes me happier, and people have asked for it before for other things. I don't like the name but that's just syntax.


(Matthew Johnson) #9

It makes me happier to! :slight_smile: I didn't pitch extending the protocol because I didn't want these factories visible on the all conforming types. I didn't think about introducing a new kind of extension, thanks @John_McCall!


(John McCall) #10

Yeah, I hope it's clear that the name was just a throwaway. It sort-of works and sort-of doesn't.


#11

I like this idea :smiley:, Allowing extension to directy extend protocol is something we can’t do just yet, and compiler should be quick to check only those. A few QQ here:

  • (If we decide to keep the name) Since static extension is extending the actual protocol, it would only make sense to add static func/var (is it?), should we drop static keyword inside?
    • It would feel as though static is applied to all declarations inside, which matches the mental model for access control keywords as well (private/public).
  • Could we constrain static extensions the same way we do to normal extension (e.g. with where clause)?
    • Allowing constraint could be problematic since compiler must proof that one where clause is a subset of another (I don’t know if future where statement could hinder such checking).
    • Disallowing constraint, may limit its usability, but I don’t know to what extend.

(John McCall) #12

No, it's sensible to add instance members in a static extension, which is one of the reasons that that name isn't very good, as Jordan pointed out.


#13

What would be the different between instance members declared in extension vs static extension?


(John McCall) #14

An instance member declared in a static extension would only be callable on values of the protocol type. Among other things, this could eventually lead to a sensible and fully-general way to allow protocol types to themselves conform to other protocols even if they e.g. have associated types or protocol requirements that refer to Self.


(Matthew Johnson) #15

True, although static extension would be pretty cool as an independent feature that was also allowed on concrete types. It would make "empty enums are ML structures" style programming a lot more convenient by eliminating the need to write static everywhere.

I suppose protocol extension is out for grammar reasons, right? Grammar aside, that basically describes what you have in mind, doesn't it?

extension on a protocol adds members to all conforming types as well as the protocol existential and generic types constrained by the protocol. We're talking about something that does the latter without doing the former.


(Matthew Johnson) #16

I really like where this is going! :slight_smile:


(John McCall) #17

I don't think we're talking about adding members to generic types constrained by the protocol, just the protocol existential.

I suppose these members would be inherited by protocol compositions, although we would need to prevent mutating members from being so inherited.


#18

If I may digress, it does sound like generic protocol to me. Is there a reason we don’t have that as part of the language?


(John McCall) #19

I don't think that's related to this, no.


(Jordan Rose) #20

Arguably this does not add members to the existential; it adds them to the type of MyProto.self. I guess it could add members to existentials, but then I don't think it's (as?) defensible to not have those members available on concrete types.