Reconsidering the 'Top-Level' Restriction on Placeholder Types

I pretty much agree with @codafi's position (though @codafi feel free to correct me if this seems mischaracterized) that _.self is, at best, only marginally more inscrutable than similar forms such as (_, _) or (_) -> _. Already, there are undoubtedly situations where it's 'obvious' that the specified type is going to be a two-element tuple, or a function of a single argument, but the element types/parameter and result types are non-obvious. The restriction on top-level placeholders only saves us from one specific issue, where the top-level type itself provides all the context needed.

As you note, we can already write Some<_, _> (or even without SE-0315, just Some), so it's not clear to me that allowing one more step of inference makes the difference between "clear because the type was specified in source" and "extremely inscrutable."

If it’s inscrutable, then the option to expand it out to a full type is always at your disposal. Recall, the underscore represents a point at which type inference has deduced a type that fits the current context.

1 Like

I’d say that (_, _) and (_) → _ are very similar to Some<_, _>: the “top-level types” are tuple and function, respectively. That small amount of information makes a big difference in terms of readability.

Furthermore, _ usually means that something is accepted and discarded.

let _ = discarded

switch someValue {
case _: break
}

noOp: if let _ = someOptional {
  break noOp
}

(1...3).forEach { _ in
  return
}

In the context of a type, it’s not hard to interpret it as a placeholder. I’m not sure that would hold true without that context.

I do feel a bit weird about _.self because the .self is still a bit of a kludge. There’s nothing type-y to me about .self (it shows up in the identity key path too), so allowing _ there feels like allowing it for any singly-inhabited type. Or not singly-inhabited, even: BaseClass.Type has multiple inhabitants; there’s just an obvious default choice.

Put another way, if types-as-values weren’t written with .self allowing a bare underscore in the parameter list would be weird. I get that these functions that force specifying a type can be too strict, but I’m not sure this is how I’d want to address that anyway.

3 Likes

Yes, I'm very interested in the eventual implementation of SE-90. In fact, I'm about to create a post inquiring about its actual fate. The current status is accepted, but deferred since, I think, 2016? How would SE-90 affect the current pitch?

Actually, I'm not sure if the deferred status means accepted with only the implementation deferred, or if the discussion itself is deferred. I remember it as the first. Someone please correct me if I'm wrong.

IIRC, "deferred" in the lead-up to Swift 3 meant basically, "out of scope for the next release, but not outright rejected as a future direction for Swift."

1 Like

Yeah, I think that's the case, indeed. Anyways, I don't have much time right now, so maybe I'll send a follow-up post later.

While refactoring Graphiti to add support for async/await I came to realize that its API would benefit from a parameter that explicitly defines the output type of a GraphQL field. The API goes like this:

Type(User.self) {
    Field("name", of: String.self, at: \.name)
}

Previously, the API was like this:

Type(User.self) {
    Field("name", at: \.name)
}

We did it this way because we can infer the field type from the \.name keypath. However, the new API is much better, because it is more explicit. It allows readers to better understand the GraphQL schema without having to chase around code from the keypath.

If I understand correctly, the current pitch would allow:

Type(User.self) {
    Field("name", of: _.self, at: \.name)
}

Which basically devolves the new API into the old one. That's unfortunate, but if the user of our API really decides to do that, what can we do?

On the other hand, I'm quite interested in SE-90, because it would allow a much leaner API that is even closer to the GraphQL SDL.

Type(User) {
    Field("name", of: String, at: \.name)
}

And SE-90 with the current pitch could allow:

Type(User) {
    Field("name", of: _, at: \.name)
}

Honestly, I can't yet decide how I feel about this. I think this is better than _.self? _.self is really weird, IMO. I guess, all of this just reinforces my interest in SE-90? Have to mull it over and follow up later on.

Just to make sure I'm clear. If I had the power, as an API designer, I would love to be able to make sure users of the API always define the field type explicitly. This would entail either not accepting this pitch or allowing API designers to forbid placeholders in an ad-hoc manner.

I do not think we should reject the current pitch just because some API could suffer from it. There's the possibility that other APIs would benefit from it. I just gave an example of an API that suffers.

I also do not like the idea of annotating a parameter to disallow placeholders. Seems too much, but maybe it could become justifiable if more examples like Graphiti's arise?

Of course, another possibility is to just learn to live with the possibility of the placeholder there and add as many warnings as possible in the documentation stating why it is important to explicitly define the type in that context.

1 Like

Even without this pitch, users could override this behavior on an ad-hoc basis by wrapping the API in a generic function which would propagate type information almost identically to how placeholders do and allow for the use of the API without even a _.self to indicate that inference is being invoked.

Granted, this is a much ‘heavier’ process, and perhaps less accessible to novices who might find the mechanics of generic functions more daunting than the use of placeholders. However, we don’t in general protect against the problem that API clients may via extensions make things implicit that the API author intended to be explicit. I’m not sure that top-level placeholders are the place to draw that line, if we want to at all.

3 Likes

Right, but we do in general avoid exposing features that permit such things directly at the call site; this was a large part of the rationale for not even considering optional labels for first trailing closures for review (which, obviously, I disagree with).

3 Likes

Fwiw, I would love it if this would work as well. I had in the past proposed .(…) as shorthand for .init(…), but to have it just be another use of the placeholder would be very elegant imo.

view.frame = CGRect(origin: CGPoint(), size: CGSize(width: 14, height: 35))
view.frame = .init(origin: .init(), size: .init(width: 14, height: 35))
view.frame = _(origin: _(), size: _(width: 14, height: 35))
Terms of Service

Privacy Policy

Cookie Policy