Reconsidering the 'Top-Level' Restriction on Placeholder Types

SE-315 was accepted with a notable restriction on the positions where placeholder types could be used. To quote the text:

An earlier draft of this proposal allowed for the use of placeholders as top-level types, so that one could write

let x: _ = 0.0 // type of x is inferred as Double

Compared to other uses of this feature, top-level placeholders are clearly of more limited utility. In type annotations (as above), they merely serve as a slightly more explicit way to indicate "this type is inferred," and they are similarly unhelpful in as casts. There is some use for top-level placeholders in type expression position, particularly when passing a metatype value as a parameter. For instance, Combine's setFailureType(to:) operator could be used with a top-level placeholder to make conversions between failure types more lightweight when necessary:

After having hacked on some better diagnostics for placeholders in invalid positions, I believe that this restriction is unnecessary and should be relaxed. Placeholders in expression position, regardless of what context they are found in, communicate the user's intent to interact directly with the type inference machinery to fill in some part of their program. In static program text, I agree it can be of diminished semantic importance to any readers, but while editing Swift code the ability to express the intent that you wish for the type checker to give you, the author, a helping hand is invaluable.

Further, to take the example given in the proposal:

However, as Xiaodi Wu points out, allowing placeholders in these positions would have the effect of permitting clients to leave out type names in circumstances where library authors intended the type information to be provided explicitly, such as when using KeyedDecodingContainer.decode(_:forKey:) . It is not obviously desirable for users to be able to write, e.g.:

self.someProp = try container.decode(_.self, forKey: .someProp)

If the client intends for this to be explicit, then we (as tooling authors) should offer the tools to make that so. The IDE should offer either "soft" expansions (a la option-clicking program elements to view their types) or "hard" expansions like refactoring actions to replace these placeholders with their inferred types. I have already submitted a patch to do just that for most other kinds of placeholder types and have filed rdar://82837396 to have the refactoring actions expanded.

Finally, this restriction is currently implemented inconsistently in the compiler. Here's the current state of affairs:

protocol Publisher {
    associatedtype Output
    associatedtype Failure
}

struct SetFailureType<Output, Failure>: Publisher {}

extension Publisher {
    func setFailureType<T>(to: T.Type) -> SetFailureType<Output, T> {
        return .init()
    }
}

let _: SetFailureType<Int, String> = Just<Int>().setFailureType(to: _.self) // expected-error {{type placeholder not allowed here}}
let _: SetFailureType<Int, [String]> = Just<Int>().setFailureType(to: [_].self) // But here it's fine?
let _: SetFailureType<Int, (String) -> Double> = Just<Int>().setFailureType(to: ((_) -> _).self) // And here?
let _: SetFailureType<Int, (String, Double)> = Just<Int>().setFailureType(to: (_, _).self) // And here?

To remedy this, I propose we just relax it. The only placeholders that should be diagnosed are those for which insufficient context exists to infer a reasonable type to fill the placeholder in. This brings placeholder types exactly into line with their implementation in the type system as typed holes.

12 Likes

You had me at hello!

Thanks for writing this up @codafi! I was hoping that there would be a follow-up discussion focused on this specific part of placeholder type behavior. I remain in favor of this change, but thought it was subtle enough to justify a bit of targeted discussion.

FWIW, I think that's perfectly consistent based on the stated rule from SE-0315: that a type cannot be just a placeholder, but point taken that these other forms don't provide dramatically more context. Like I said, I consider relaxing this restriction a win. :slight_smile:

3 Likes

I'm not sure what this means. What difference in intent do you see between let x and let x: _? They both allow the compiler to infer the type of x. The user adding _ seems redundant, as they didn't change behavior at all. So can you explain what you mean here?

2 Likes

While editing, the presence of type placeholders indicate places where I need to go back and rethink some part of my program I'd like to be explicit later, but for which I currently lack the ability, the foresight, or the context to actually fill in at the moment. In contextual position like this, there is no semantic difference in the static text of the program between the two declarations, but we must consider more modalities than just that. Our traditional answer to this is editor placeholders which are unconditionally diagnosed in the source text (outside of playgrounds mode, where they function like typed holes, ironically).

So essentially you're saying users could use _ as a reminder to go back and fill in an explicit type once they figure out what it should be? I don't know why anyone would do that who wasn't working around a compiler performance issue, so I'm not sure it needs to be something explicitly supported by the language. Users can almost as easily just a comment or other todo.

2 Likes

Both of which lack semantic context, and do not provide access to the resources of the constraint system.

But should it be unconditionally banned is the question. In many cases, the type system has the context it needs to infer the structure of the program that needs to fit the hole, but by unconditionally banning type placeholders in these positions we're saying those inference sources are worthy of being removed from the program.

What context or resources are gained by users adding the _? Are you talking about context or resources used by tooling the user may be using? I don't see what you mean for users just writing code here.

3 Likes

Consider the Combine case presented above. With big chains of operators I often find myself in a position where I know what I want is an AnyPublisher<_, _>, so I get through writing the chain

    let _: AnyPublisher<_, _> = URLSession.shared
      .dataTaskPublisher(for: url)
      .receive(on: DispatchQueue.main)
      .compactMap { data, _ in PlatformImage(data: data) }
       // Not good practice, but fine for debugging 
      .mapError { e -> Never in fatalError("\(e)") }
      .eraseToAnyPublisher()

Then I come to discover I want to return a particular error type, or I flatMap in a publisher into the chain that needs a compatible error type. Either way, I can just write

    let _: AnyPublisher<_, _> = URLSession.shared
      .dataTaskPublisher(for: url)
      .receive(on: DispatchQueue.main)
      .compactMap { data, _ in PlatformImage(data: data) }
       // Not good practice, but fine for debugging 
      .mapError { e -> Never in fatalError("\(e)") }
      .setFailureType(to: _.self)
      .flatMap { /*SomeOtherPublisher()*/ }
      .eraseToAnyPublisher()

And I'm free to hack on whatever needs to go into that flatMap without having to worry about the failure type there because it can be inferred from context.

Combine is a particular heavy example of this kind of pattern, but it's certainly very practical.

Of course. However, I don't consider let x: _ and let x: AnyPublisher<_, _> to be equivalent. And in your example, simply providing let x infers the whole type. Unless you're saying let x: AnyPublisher<_, _> isn't possible right now, which would be very surprising.

Perhaps we've been talking past each other here. At issue isn't the _ in let x: _, it's the _ in .setFailureType(to: _.self).

Perhaps my limited quote of your first example was misleading. I was looking at the let x: _ = 0.0 // type of x is inferred as Double in your quote as an example of what you were talking about. Perhaps you can summarize the actual restriction(s) you'd like to lift?

Good idea. The following constructions are banned under SE-0315 but would be allowed under the relaxed regime:

/// The contextual type [String: Int] propagates to the underscore in expression position
func dictionary<K, V>(ofType: [K: V].Type) -> [K: V] { [:] }
let _: [String: Int] = dictionary(ofType: _.self)

/// There is sufficient contextual information to instantiate the underscore to `Int`
let _: Int.Type = _.self

/// Similarly, there is enough information to instantiate the underscore to String
let _: SetFailureType<Int, String> = Just<Int>().setFailureType(to: _.self)

The following code remains ambiguous under the new regime AND SE-0315 as written

// Despite the contextual type, we cannot instantiate the type `Int` into the underscore because of missing inference rules
let _: Int = _()  // error: type of expression is ambiguous without more context
let _: Int = _.init() // error: could not infer type for placeholder

// The contextual type is insufficient to deduce the type of the middle underscore in general
let _: SetFailureType<Int, String> = Just<Int>().setFailureType(to: _.self).setFailureType(to: String.self) // error: generic parameter 'T' could not be inferred

// We're missing pattern rewriting rules here
switch [Int]() {
case is _: break // type placeholder not allowed here
case is [_]: break // type placeholder not allowed here
case is () -> _: break // type placeholder not allowed here
case let y as _: break // type placeholder not allowed here
case let y as [_]: break // type placeholder not allowed here
case let y as () -> _: break // type placeholder not allowed here
}

The following code works under both:

// Structural usages of underscores in casts with enough contextual information are legal
let losslessStringConverter = Double.init as (String) -> _?

// Structural usages of underscores with contextual information
let _: SetFailureType<Int, [String]> = Just<Int>().setFailureType(to: [_].self)
let _: SetFailureType<Int, (String) -> Double> = Just<Int>().setFailureType(to: ((_) -> _).self)
let _: SetFailureType<Int, (String, Double)> = Just<Int>().setFailureType(to: (_, _).self)
3 Likes

Thanks, that makes much more sense.

Personally I like being able to use _.self, as the reasoning for authors disallowing inference has never really made sense to me.

2 Likes

I think using a placeholder for a top-level type should cause a compiler warning. It’s not ambiguous, but it is completely pointless and harms readability.

Compiler errors should be reserved for code that cannot be unambiguously interpreted according to the language rules, rather than code that is merely ill-advised.

1 Like

In a type annotation this might be true, but I don't think it's true for type expressions. In the setFailureType(to:) examples, it's possible that you need to set the failure type to Some<Long, Complex<Generic.Type>>> that you don't want to type out, but omitting the type entirely is not an option since you have to pass some parameter. In that case, being able to write _.self instead of the full name of the noisy generic type might actually help readability.

Why would that be a top-level placeholder?

"Top-level placeholder," as used in SE-0315, refers to any type which consists of only a type placeholder, and nothing else. So the placeholders used in let x: _ = ..., ... as _, and _.self are all considered "top-level" because there are no other components to the type.

1 Like

Couldn’t you replace Some<Long, Complex<Generic.Type>>> with Some<_, _>? It’d be easier to read, I think.

1 Like

Well, suppose it's SomeLongComplexGenericTypeName, then. :slight_smile:

Point is, there are situations where you can't simply omit a top-level placeholder, so some type would have to be specified. In the case of let x: _ = ..., we can offer a fix-it of "remove ': _'"—not so for some uses of _.self.

1 Like

My problem with it is that _.self is extremely inscrutable. If a type must be specified, it is presumably for a good reason.

I feel that extremely complicated types are better dealt with using type aliases, opaque types, and other existing features.

3 Likes
Terms of Service

Privacy Policy

Cookie Policy