[Pitch] Lifting restriction on explicit specialization in function calls

This was previously discussed here, but the discussion seems to have dropped off:

In the time since that was posted, while investigating how difficult it would be to support, I found that the existing constraints fully support this feature, and it's purely restricted syntactically.

I would like to propose we lift this restriction, which would allow using type inference in more places, e.g. specializing function references.

For example, ideally I would like to be able to write a function like this, which uses contextual information to determine what the decoding output.

final class MyDecoder: Decoder {
    func decode<T>(from data: Data) throws -> T
}

In order to use it, I either have to pass it to a context which I know the type is derivable, or provide a type annotation. In both cases, I have to somewhat play constraint solver on behalf of the compiler, lining up the types such that the constraint system is satisfied:

let value:  MyType = try decoder.decode(from: data)

// or pass it to a function:

func processValue(_ value: MyType) { ... }
processValue(try decoder.decode(from: data))

or as a coercion

let value = decoder.decode(from: data) as MyType

But what I really want is to be able to provide the type arguments the same way I provide the function arguments, using the same syntax the type parameters are declared with:

let value = decoder.decode<MyType>(from: data)

This is already supported for type instantiation when resolving constructor declarations:

let value = Array<MyType>()

And you can form partially applied functions referencing these instantiated constructors:

struct GenericValue<T> { var value: T }
let createGenericValue = GenericValue<Int>.init

The canonical workaround that has become accepted in Swift is to provide the generic parameter as a metatype argument:

func decode<T>(_ type: T.Type, from: Data) throws -> T

But that requires providing MyType.self, which is not something someone new to Swift would know to do. Many languages support this kind of explicit specialization for functions at the call site: C++, Java, Rust, C#, to name a few.

I think this restriction should be lifted, and I also think this should be preferred over the metatype parameter versions of generic functions like this. There's still value in taking meta types, like withTaskGroup's returning: argument label. That said, I think there's an argument to be made for labeled generic parameters as well.

I have an experimental PR up now to lift the restriction and update the tests. It generally does what you'd expect.

Thoughts? I would also appreciate more information if I'm missing something about the necessity of the restriction.

20 Likes

This restriction is super confusing coming from other languages. If it could go away I, for one, would be happy.

6 Likes

+1. I have specifically seen this exact issue trip people up when coming to Swift from TypeScript (or vice versa).

1 Like

I agree that this needs improvement, but I think other languages handle it poorly, and the right solution for Swift is to just get rid of the .self. That way, you can use labels, and not have to fill in type placeholders.

E.g. given…

func f<T, U>(
  goodLabel: T.Type = Int.self,
  thingThatIsBestInBetween: some Any,
  evenBetterLabel: U.Type = U.self
) -> (T.Type, U.Type) {
  (T.self, U.self)
}

_ = f<_, Void>(thingThatIsBestInBetween: "thing")

is better than

_ = f(thingThatIsBestInBetween: "thing") as (_, Void.Type)

but

_ = f<_, Never>(thingThatIsBestInBetween: "thing")

is worse than

_ = f(thingThatIsBestInBetween: "thing", evenBetterLabel: Never)

I agree, and if we lift this restriction, we can also remove the logic to enforce that every type parameter appears within a function’s type. That is, today we reject this:

func f<T>() {}

But this is quite silly because there is no reason not to accept this, and the check for this condition has itself had bugs in the past.

13 Likes

Is there? I don't think people care where the labels go as long as they're available.

_ = await withTaskGroup(of: Int.self, returning: Bool.self) { group in
_ = await withTaskGroup<of: Int, returning: Bool> { group in

The definitions would be less noisy too—a placeholder's only possible value is itself so you don't need defaults:

@inlinable public func withTaskGroup<ChildTaskResult: Sendable, GroupResult>(
  of childTaskResultType: ChildTaskResult.Type = ChildTaskResult.self,
  returning returnType: GroupResult.Type = GroupResult.self,
  isolation: isolated (any Actor)? = #isolation,
  body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult {
inlinable public func withTaskGroup<
  of ChildTaskResult: Sendable, returning: GroupResult
>(
  isolation: isolated (any Actor)? = #isolation,
  body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult {
1 Like

May I ask why the label is required? Suppose the pitch is implemented, why does the following (without label) not work?

@inlinable public func withTaskGroup<
  ChildTaskResult: Sendable, GroupResult // type parameters have no labels.
>(
  isolation: isolated (any Actor)? = #isolation,
  body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult {
  fatalError()
}

I was thinking at the time (though my post doesn't do a good job of explaining it) of this declaration:

func withTaskGroup<ChildTaskResult, GroupResult>(
    of childTaskResultType: ChildTaskResult.Type = ChildTaskResult.self,
    returning returnType: GroupResult.Type = GroupResult.self,
    isolation: isolated (any Actor)? = #isolation,
    body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult where ChildTaskResult : Sendable

Which, when used with this syntax, would look like:

withTaskGroup<Int, Bool> { ... }

Which I do think is less clear than

withTaskGroup(of: Int.self, returning: Bool.self) { ... }

3 Likes

I think the reason to not accept it is that 999 times out of 1,000, it would be because you made a boo and misspelled the placeholder, and not because you meant it.

3 Likes

Could it be downgraded to a warning, then?

1 Like

how would you silence the warning?

1 Like

Ah sorry, I neglected to say that typos don’t seem like the kind of thing we should try too hard to diagnose (IMO), because they likely won’t compile or be callable as you expect anyway. I imagine you would notice fairly quickly.

So if we drop that, the remaining value is in diagnosing genuinely unnecessary generic params.

If we wanted to keep that, we could replace it with something that actually diagnosed unnecessary parameters, that thing should probably be based on the function body and would emit warnings that you could easily suppress with “_ = T.self”. But an unused generic parameter would be a warning rather than an error.

Sorry that was my train of thought and I didn’t explain it.

1 Like

For full function calls, I don't think there's a fundamental reason to prevent this. For unapplied function references (like let f = someFunction<Int>), you have the same angle bracket syntactic ambiguity problem as for type references that requires them to always be followed by a call or member reference like .self, but we could subset that out.

1 Like

This is only a problem for an unapplied function reference that doesn't specify argument labels, correct? I believe it is almost always possible to specify labels, except when the referenced function takes no arguments at all. Perhaps if we solved that, we could deprecate unlabeled function references altogether (they also produce large overload sets, etc). On the other hand, you could argue that foo<T>(_:_:) or something is already too verbose.

1 Like

Yeah, the disambiguation rule relies on the closing bracket being followed by a . or (, so function references with labels would be OK. I wouldn't mind deprecating unlabeled function references (though we'd then need to make up a syntax for referring to a function with no parameters without calling it), but it could be a reasonable starting position to say they're necessary for function references with explicit generic arguments.

1 Like

(This is SR-3550/issue #46138.)

1 Like