[Pitch] Use “some” to express “some specialization of a generic type”

This is definitely needed. The asymmetry we have at the moment doesn't make any sense.

// Compiles.
func ƒ(_: some BidirectionalCollection & RandomAccessCollection & ExpressibleByArrayLiteral) { }

// Reference to generic type 'Array' requires arguments in <...>
func ƒ(_: some Array) { } 

That said, I can't tell if your examples should actually compile—is some Position really meaningful enough?. It would be nice to have examples that either don't rely on code we don't have, or if you could provide the code.

2 Likes

I see prefixing a generic type with some as a readable way of propagating inference of type parameter conformances. I realize this depends on a some _ placeholder capable of doing this for a single type parameter, which perhaps should have been the core of this pitch. Given a type struct Foo<A, B> where A: P, B: Q, some Foo would mean some Foo<_, _> which would mean Foo<some _, some _> which would mean Foo<some P, some Q>.

To be clear, some Foo<Int> would be rejected, right?

Yes, I think you would have to specify both type parameters. Maybe when generic opaque type constraints are available, it would make sense to use the same syntax in this case as well and write something like: some Foo<.B == Int>, which would be the same as some Foo<_, Int> or Foo<some _, Int> with the pitched syntax and Foo<some P, Int> with the current syntax. I think some Foo<Int> only makes sense if there exists an analog to primary associated types.

This has occurred to me as well, but I suppose it only makes sense for input parameters, e.g. some Array could not be a return type. The function implementer would know what Element is but as a caller you wouldn't. Unless the idea is that the actual type is revealed to you implicitly:

func foo() -> some Array { 
   [1, 2, 3]
}

let x = foo() // x is now [Int]

EDIT:

Turns out this already works for some Collection, so I guess it would make sense for some Array as well.

  func foo() -> some Collection {
    [1, 2, 3]
  }

  func bar() {
    let baz = foo() // some Collection in XCode
    print(type(of: baz)) // prints Array<Int>
  }

For now. I haven't seen people clamoring for it but that doesn't mean it's not useful for anything.

E.g. I never found a use for some Sequence, even though I was just able to get rid of a bunch of usage of AnySequence in favor of some Sequence<ConcreteType>. And some Sequence still compiles. So why shouldn't every generic type as well?

I like the idea a lot! Would love to hear any doubts or counter arguments, but my initial reaction is +1

I'm starting to recognize that people want the any variant of this, as well as the some one.

1 Like

What if you could write:

func substring(_ range: Range<some>) -> Substring

and the constraints on the generic parameters would be inferred from Range's requirements? That avoids the need to restate the bounds, while also keeping the number and position of implied generic parameters easy to read for someone looking at the declaration. We infer bounds on T when you write func foo<T>(_: Range<T>), so this seems like a nice consistent shorthand.

3 Likes

This already works via the usual requirement inference mechanism:

func substring(_ range: Range<some Any>)

So if you parsed some as some Any it might just work.

2 Likes

The interesting thing I would like is the any variant:

struct Generic<T> {
  let value: T
}

func something() -> any Generic {
  if Bool.random() {
    return Generic(value: 123)
  } else {
    return Generic(value: "hello world")
}

An existential type any P & Q is really something like this, we’re that syntax to exist: any <T where T: P, T: Q> T; that is, it’s a container for a value of type T which is a generic parameter in a local generic signature with some requirements imposed on it. Similarly any Sequence<Int> becomes any <T where T: Sequence, T.Element == Int> T.

Your any Array would become any <T> Array<T> in the more general syntax; so it’s a container storing an Array<T> (not just T) and the T is type erased. You could also have more than one erased parameter, for eg any Dictionary.

Not sure we’ll ever get “fully generalized existentials” in the surface language, but this is fact how they’re modeled internally in the compiler.

(If I ever get around to finishing it, Part II of Compiling Swift generics has a chapter on existential types).

9 Likes

please can we surface this? this would be immensely helpful.

2 Likes

We need this kind of control sometimes…

func ƒ(_: [String: some Any]) { }
func ƒ(_: [some Any: String]) { }

…but otherwise, I don't think it's so good when there's more than one placeholder type.

func ƒ(_: [some Any: some Any]) { } // compiles
func ƒ(_: some Dictionary) { } // doesn't; would be better

Being able to put some in either place for types with a single generic placeholder, as per your whim, seems consistent with case-binding:

switch "🇸🇴" as Optional {
case .some(let _): break
case let .some: break
default: break
}

I think that's where I disagree it's better. Although Dictionary is a common type that people are familiar with, seeing some Foo for an unknown type or protocol would become a lot more ambiguous, since it could be either introducing one generic parameter bound to a protocol named Foo, or any number of generic parameters corresponding to the parameters of a generic type Foo.

A reasonable midpoint might be to allow for variadic implicit generic parameters, so you could write

func f(_: Dictionary<some...>)

or whatever syntax we decide for variadic type variables as a way to keep it clear that multiple type parameters are being introduced.

4 Likes

This probably deserves its own thread, but occasionally I find myself reaching for TypeScript's infer operator. Like let, infer creates a new binding, but that binding is to a type. Here's an example I just encountered:

    // Inside a generic struct
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Key.self)
        if Value.self == Optional<infer Wrapped>.self {
            value = try container.decodeIfPresent(Wrapped.self, forKey: key)
        } else {
            value = try container.decode(Value.self, forKey: key)
        }
    }

The design of Codable aside (as there might be a better way to do this), I do occasionally wish something like this were possible.

i wouldn’t want to write or read some Dictionary, because it is discarding some relevant information about the two type parameters, namely that the Key is Hashable. so i would prefer to write it:

func ƒ(_:[some Hashable: some Any]) 

moreover, i rarely want to abstract all the way down to [some Hashable: some Any] at all, more likely i would write APIs that look like

func ƒ(_:[some MyKeyProtocol: some MyValueProtocol]) 
1 Like

Multiple primary associated types (along with unlimited non-primary ones) are already supported with a single some, so the "any number" part wouldn't be new.

import protocol Combine.Publisher
func ƒ(_: some Publisher) { } // is shorthand for
func ƒ(_: some Publisher<some Any, some Any>) { }

What would add ambiguity would be a some coming before something that is not a protocol or superclass. Maybe that's not good. (Although PascalCase is the convention for so many different types of things, I'm inclined to lean towards, "just option-click it if you need to learn about it" the way we've always had to.) Can any be done differently though? The some stuff here is sugar, but the any is a missing feature.

I'm not sure what you mean by "discarded"—you can't subvert the requirement.

That works already though—go write it! :partying_face:

I hadn't thought before today about how asymmetric it is that only the last line here does not compile.

protocol Protocol<T> { associatedtype T }
final class Class<T> { }

func ƒ(_: some Protocol<some Any>) { }
func ƒ(_: some Class<some Any>) { }

func ƒ(_: some Protocol) { }
func ƒ(_: some Class) { }