Make dot shorthand work with generics and existentials

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

2 Likes

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.

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.

1 Like

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

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.

7 Likes

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.

1 Like

I really like where this is going! :slight_smile:

2 Likes

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.

2 Likes

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?

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

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.

3 Likes

Well, you certainly could make them available on concrete values/types, but I think in general users wouldn't want them to be, and it's easy enough to define them both ways if necessary.

Wouldn't that be required to make my sample code work?

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)

fold takes M: Monoid and .sum is used to drive inference of the type. This is how the workaround works, but that requires taking Sugar<M> instead of M so it breaks when users want to pass a value of some monoid that is stored in a value.

I'm not following this. I can't see why these would be any more limited than protocol extensions. The only difference is that the members declared in these extensions would not be visible on the concrete conforming types.

Wouldn't instance members here be static members on the protocol, thus this view would imply what @Lantua suggested about not using static inside the extension? I was wondering if metatype extension would make sense. That would also be useful on concrete types and would also facilitate the ML style when used with empty enums.

Yeah, I would be strongly opposed to making them visible on the concrete types. This is exactly the reason I didn't suggest using today's protocol extensions for this. I absolutely do not want static var sum: Sum (declared in an extension of Monoid) visible on Product, etc. That would lead to weird symbols polluting the namespace of types whenever these sugar factories are used.

I misunderstood what you were trying to do. That doesn't really fit with what I was thinking, then, no.

Ok, huh... Does it fit with @jrose suggested then? Would metatype extension P be a reasonable syntax for that? What are your thoughts in general about this? The goal is to use dot shorthand to drive type inference as well as to construct values of concrete conforming types in a DSL-like fashion.

There's definitely a tricky bit here, in that we don't want to change the behavior of this code:

func getMinFromElementForSomeReason<T: BinaryInteger>(_: T) -> T {
  return M.min
}

let x: Int = getMinFromElementForSomeReason(.max)
// That should be Int.max, even if BinaryInteger.max were defined.

I'm no longer sure this exactly makes sense for your original use case. The only reason it works is because the concrete types conforming to your Monoid protocol get instantiated, which isn't even strictly necessary for normal Monoid use.

I assume you meant the body to return M.max. How would my pitch break that? API visible on concrete types would not be visible at all, only API defined on the protocol itself or in a metatype extension. If members declared on the protocol itself conflict with members declared in a metatype extension I would expect to get a duplicate symbol declaration error or something like that.

If you want to reference a member of a concrete type then dot shorthand would not be available for the reasons discussed in the beginning of this thread, so you would have to use Int.max to reference it. I don't think that is a problem.

I suppose it could be confusing that static factories declared on the protocol itself that return Self would not participate in the dot shorthand, but if you think about it that wouldn't make sense: dot shorthand requires a concrete type to be known and a static protocol member returning Self explicitly does not specify a concrete type. This shorthand feature is basically useless if it can't be used to drive inference.

It isn't necessary for many monoids but some monoids do benefit from dynamic parameterization (via an initializer). And of course monoid is just the concrete example for the sake of discussion. Many abstractions will benefit from both this sugar and from dynamic parameterization of instances. (Also note that I also pitched support for ML signature style protocols where there is no instance and the sugar is only used to drive metatype passing).

No, I specifically wanted the body to return T.min (but wrote M, oops). That is, the call-site choice for T is entirely specified by the type context, and even if I had

__metatype extension BinaryInteger {
  static var max: UInt64 { return ~0 }
}

the result still has to be Int.

Ok, so I think this is what you meant:

func getMinFromElementForSomeReason<T: BinaryInteger>(_: T) -> T {
  return T.min
}

let x: Int = getMinFromElementForSomeReason(.max)
// That should be Int.min, even if BinaryInteger.max were defined.

Is that correct? I agree that the behavior of this code should not be changed. We should either say that explicit type annotations take precedence over shorthand-driven inference. We could also extend that to cases where there are other circumstances that fix the type T as below:

func getMinFromElementForSomeReason<T: BinaryInteger>(_: T, _: T) -> T {
  return T.min
}

let int: Int = 42
let x = getMinFromElementForSomeReason(.max, int)
// That should be Int.min, even if BinaryInteger.max were defined.

The other option in cases like these would be to produce an ambiguity error so the behavior would not change but some code might become ambiguous in the presence of metatype extensions and require an explicit Int.max to resolve the ambiguity.

Do you still feel this way? It seems to me like maybe metatype extensions with dot shorthand driven by protocol metatype extensions might be a very nice solution to the use case.

I would like to see some kind of feature like this. In my own codebase, I have wanted protocol metatype properties with less complex cases, in the following approximate pattern:

protocol CustomDelegate { 
    func complexRequirementsHere()
}
class DefaultCustomDelegate : CustomDelegate {
}

func performAction(withDelegate delegate: CustomDelegate) {
    ....
}

performAction(withDelegate: .default) // versus currently performAction(withDelegate: DefaultCustomDelegate())

The way I imagined this feature being expressed is something like

extension CustomDelegate.Type {
    var `default`: DefaultCustomDelegate { return DefaultCustomDelegate() }
}

It feels like it falls out quite naturally, with the goal of better call-site readability. Admittedly, I haven't thought of all the possible applications of this, like more complex generic expressions, but I thought I'd contribute another motivating use case (and syntax suggestion) to the thread.

Last year, I identified where this could potentially be used to improve the RandomNumberGenerator API.

1 Like