Reverse generics for argument type?

No, I'm saying [some Hashable: any Numeric] . You can make this type like:

func someFunction() -> [some Hashable: Numeric]  { /* ... */ }

For me it seems possible to do this if this is possible.

And if we agreed to use any for existential type, it becomes any Hashable, so, we would have this.

func someFunction() -> [some Hashable: any Numeric]  { /* ... */ }

If there is fault to use dictionary, I can say the same thing for tuple.

func someFunction() -> (some Numeric, any Numeric)  { /* ... */ }

Doesn't it make sense?

1 Like

We already have reverse generics/opaque types in argument position. Just write the following in a playground and check the type of f:

protocol Protocol {
  func foo(_ p: Self)
}

struct Struct: Protocol {
  func foo(_ p: Self) {}
}

let s: some Protocol = Struct()
let f = s.foo
// f is (some Protocol) -> Void
// f is (some Protocol (type of 's')) -> Void

As you said, unlike generics, with reverse generics the type is decided by the callee (in this case is Self, which is deduced by the protocol interface).

Thus, this code snippet doesn't make sense:

You cannot use an opaque type without actually providing the underlying type (directly or by means of the context in which the function is declared). That function shouldn't be callable by design (you know that it accepts a specific conforming Numeric type, but you don't know which type).

2 Likes

It's my fault, but this is quotes from here. I fixed the expression.

No, in this case it is deduced by the caller of f. The type some P in argument position isn't decided by the callee, it is decided by the (nth) caller, hence we get into trouble not considering some P as existential type.

In order to call foo you need an instance of a conforming Protocol type, in this case it's s. Protocol.foo is of type (Self) -> Void, and Self in this context is the type of the instance, i.e. (type of 's'). You cannot decide which type the argument should have, it's the method foo that knows and constrains the type of its argument to be (type of 's').

1 Like

I thought the some P in the parameter position is just a (proposed) shorthand for <T: P> after the function name and then using T in the parameter position.

It does make sense because the caller provides the missing information needed for the return type.

2 Likes

Yes. This can be confirmed if we make static member constraint. (EDIT: I added a test)

protocol Protocol {
    func foo(_ p: Self)
    static var value: Self { get }
}

struct Struct: Protocol {
    func foo(_ p: Self) {}
    static let value: Struct = Self()
}

let s: some Protocol = Struct()
let f = s.foo
// f is (some Protocol) -> Void
// f is (some Protocol (type of 's')) -> Void
// it works.
let _ = f(s)
// it also works.
let _ = f(.value)
//Error: Cannot convert value of type 'Struct' to expected argument type 'some Protocol'
let _ = f(Struct())

This is reverse generic argument type because no one except f knows the exact type of its argument but it still works.

Also yes. If I understand correctly, current direction is making the some P shorthand of normal generics in argument type. And I'm worried about these two things, because they seems cannot work together.

No, foo must accept any type provided, the only problem you would have is if you pass two instances of Self, you have to assure that the caller passes the same type for both some P arguments.
Anyway, the caller decides with some restrictions from the callee.

No, f doesn't know the exact type.

I must admit, the caller likewise because some P is constructed from another callee in this case here.
But for the caller, some P is then the concrete type passed by the caller to the callee ;).

However, passing Struct should also be possible.
It isn't yet as shown by you:

//Error: Cannot convert value of type 'Struct' to expected argument type 'some Protocol'
let _ = f(Struct())

However, later improvements should allow for this.

Yes, "any" feels quite natural, doesn't it? :slight_smile:

If you were going to really declare a function which can accept any Protocol, you would have spelled it as func foo(_ p: any Protocol) instead of func foo(_ p: some Protocol). If we stick with the IMHO simpler model that some is decided by the callee independently of the position (either argument or result position), we can obtain a model coherent with the current implementation of opaque types.

I don't think that treating func foo(_ p: some Protocol) as a sugared alternative to func foo<T: Protocol>(_ p: T) can lead to a sound type system. After all if it were, then you would also have the following as alternatives to the same function which is not true:

func foo() -> some Protocol
func foo<T: Protocol>() -> T

The difference between generics and reverse generics is in who decides the type, the caller in the first case and the callee in the second one. Opaque types are modeled after reverse generics and not generics.


It shouldn't. Struct may be defined in another module as private and may change among future releases of the module. If you were allowed to pass the underlying type, you would "break the contract" of opaque types.

3 Likes

Here, you use any Protocol for "normal generics", right? Or do you use it for "existential type"?

No, the latter signature is invalid for any function determining the type itself. You have to rewrite it to:

func foo() -> (T:Protocol)

where we can clearly see it is an existential.

Interesting, how would you even do that?
How does the caller at all know what to pass to the callee?

I don't understand that. F has to accept any conforming type to Protocol.
And yes, I see the irony here.

I'm interested how some P types evolve in Swift, when you really want them callee decided everywhere, then they are different to impl Trait in Rust.

My apologies, seems like I had not fully grasped the implications of normal generics vs. reverse generics in relation to using some in various locations. After reading the discussion in this thread I have now changed my post to talk about possibly using func foo(some: P) {} for reverse generics, not for normal generics.

2 Likes
func foo() -> <T:Protocol> T

Is not the same as:

func foo<T: Protocol>() -> T

the latter would reject callee site determination of the type parameter T. The former isn't a type parameter at all, it decays to a type, an existential type.

I think part of the confusion stems that some P is either sides seen as a dual to generics, otherwise some P is seen as type alias, which is contradictory.

I give you right, here. We can actually consider some P in return type position as type aliases, but for the argument position, it seems not to working anymore.
The type aliases are created from other callees than the actual callee accepting the some P type, so we actually have to deal with different type aliases.

No, I'm using any to refer to protocol types as the Swift Programming Language Guide calls them. Generics and protocol types are identical on the call site:

func foo<T: Protocol>(_ p: T) {}
func bar(_ p: any Protocol) {}

you can pass any Protocol-conforming type to both foo and bar, but the difference lays in the possibility to specialize the former.


The caller doesn't know the underlying type which can be passed and that's the point, since that underlying type might be private, therefore unreachable. However, based on the protocol interface, it can be guided to the "correct" opaque type.

private struct Private: Equatable {}

let p: some Equatable = Private()
let q: some Equatable = Private()

p == q  // error: p and q may have different underlying types

The diagnostics can help: you can write p == p, q == q or even let p2 = p; p2 == p, but you cannot use == on unrelated some Equatables.

Who decides which types == (lhs: some Equatable, rhs: some Equatable) -> Bool can accept in argument position? It's the function ==, not the caller:

protocol Equatable {
  static func == (lhs: Self, rhs: Self) -> Bool
}

The declaration of == constrains both lhs and rhs to have the same type, which is the type Self of the instance (in this case the instance is p).


With this design in place, which I'd like to remark that it's just one of the possible designs that can extend the current status of opaque types consistently, would it make sense to allow declaration of functions having opaque types in argument position? No, it wouldn't. In the declaration of the following function

func == (lhs: some Equatable, rhs: some Equatable) -> Bool { ... }

the types of lhs and rhs cannot be inferred from the declaration context. You would need to use reverse generics explicitly if they were available

func == <^T: Equatable = Private>(lhs: T, rhs: T) -> Bool { ... }

but such a function would be almost useless. T is known to conform to Equatable, it is provided by the function but it is unrelated to any other Equatable-conforming type, so you cannot have an instance of the "correct" opaque type to pass as argument.

An example in which the function can still be called has been provided by @ensan-hcl:

but it would be quite unusual to provide such a function in a module for the sake of making the parameter type private. If you want the parameter type to be private, then the function declaration should most likely be private too.

One of the key motivations for opaque types in return position was that library authors could hide the internal implementation details and instead only provide a constraint, so that library could evolve internally without breaking the API. I wonder if similar possible use case could exist for the function parameter position as well. Although maybe those cases can already be solved with some other way, like just using regular generics...

1 Like

I’m pretty sure the compiler can already specialize the existential-taking function. (Hopefully someone with more expertise in the compiler can say whether bar is guaranteed to be optimized equally well as foo.)

The real difference is that with generics you can tell the compiler about same-type constraints:

func foo<T: Protocol>(_ p: T, _ q: T) {
  // p and q are known to be the same type
}

func bar(_ p: Protocol, _ q: Protocol) {
  // p and q could be of different types
}
3 Likes

You refer to the example with two Self parameters, the only constraint the callee (i.e. ==) adds to the context is that both parameters must be the same which is currently only unique for the same variable or different aliases to the same variable.

But == doesn't specify what Self has to be for a type, that is specified by the caller, in our case it was specified by another callee with name Private which was executed in the caller.

First I wouldn't call a value an existential, this is more kinded to a variable.
Second, we could also store the proofs of some P inside the value of some P and unwrap the existential behind, but why we should do so if there is the option to optimize the boxing of some P out by simply replacing the box with the underlying type.

Yes, I see your point.
The Swift definition of existentials is more related to implementation details, my is more of abstract nature. So you are mostly correct with your conclusion.

Is it really identical? Current behavior is not totally the same. Is the conformance also the feature that would be achieved in the future Swift? (I know it's true for Error)

// Non-Existential Type (concrete type)
let value1: Struct= Struct()
// Existential Type (protocol as type)
let value2: Protocol = Struct()

let _ = foo(value1)  // possible
let _ = foo(value2)  // impossible, protocol 'Protocol' as a type cannot conform to the protocol itself
let _ = bar(value1)  // possible
let _ = bar(value2)  // possible

In some cases, this may not be possible, and a distinction must be made.

protocol Protocol {
    static func bar() -> Int
}

struct Struct: Protocol {
    static func bar() -> Int {
        42
    }
}

func foo<T: Protocol>(_ value: T) {
    let value = T.bar()
}

// What happens if call `foo` with protocol type
// `Protocol` doesn't have any implementation
let value: Protocol = Struct()
foo(value)
1 Like