SE-0244: Opaque Result Types (reopened)

In the previous review thread, I said:

I'm in favor of the feature and I think the proposal covers enough ground to be a useful addition to the language.

However, the vague syntax suggestions in the "Future Directions" section concern me. I'm worried that the -> some P syntax may be adequate for this proposal's purposes, but may not extend cleanly to uses we envision in the future. Maybe there is some larger scheme we could fit the some syntax into; if so, I'd like to see a sketch of that scheme before we commit to it here. (More thoughts on that in the Protocol<.AssocType == T> thread.)

Otherwise, I support it.

The "generics UI manifesto" has addressed my future-directions-related concerns and I support its overall vision. I also still support the feature itself.

I'm a little hesitant about the decision to introduce the some syntax with this proposal and defer the full "reverse generics" syntax for later, but I ultimately think it's the right decision. Once we support (...) -> <T: P> T syntax, people will immediately try to use it in ways the current implementation doesn't support. It would be a poor experience for users to have so many different things be straightforward to express but unsupported, and I don't think it makes sense to delay opaque result types until they can support every bell and whistle the generics system has to offer. Therefore, shipping opaque result types with only the some syntax is our best option.

I look forward to seeing some of the other results of the generics UI manifesto. some in parameter position seems like especially juicy and low-hanging fruit—I've had to explain to several different people why inout P parameters don't do what they want, and inout some P would be a very nice solution to offer them.

1 Like

Strong +1. I don't have any strong feelings on the specific syntax being proposed, so I'll leave arguing over that to other folks, but I'm very much in favor of the spirit of this pitch.

Yes absolutely.

I believe so. I'm a heavy user of Swift's generics and this feature would resolve a good portion of the issues I face. The other features listed in the Generics UI post would solve most of the rest.

I haven't

I read the generics manifesto, generics UI post, proposal, diff, most of the associated threads.

What is your evaluation of the proposal?
I would give this it a YES

Is the problem being addressed significant enough to warrant a change to Swift?
As a developer who develops and maintains a few libraries, I would say YES
Does this proposal fit well with the feel and direction of Swift?
I believe it does

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
[N/A]

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
I would like to try a try since it looks promising. However I couldn't load the toolchain linked in the Swift Evolution. Where can I get a toolchain to try this? I have a few questions but did found answers to those question in both the a reading in the SE and read through the old proposal thread.

The new manifesto makes it much clearer where this fits and and why it’s not an edge case with way too much conceptual overhead, which is how it looked before. I do think there will be a pedagogical problem, but it no longer looks insurmountable.

I’m a little bit worried that the lack of composability, especially the inability to use Optional<some T>, will be a sharp edge.

3 Likes

As was mentioned in Improving the UI of generics thread, I'd much prefer using syntax of

func makeAnimals() -> (some Animal T, some Animal U) { ... }

for naming the types, applying to both some and any types. And in both parameter and return type positions.

3 Likes

Overall +1 on the Opaque Result Types proposal. I truly appreciate the "MVP" nature of this proposal. Not trying to cram in too much in the first round of this feature. Well done!

I'm guessing you didn't read any of the follow-up posts, nor the revised proposal. A method for resolving this difficulty is explicitly mentioned as a future direction. It does no good to make a snarky comment that contains a blatantly untrue statement.

2 Likes

Please keep your comments constructive. You are welcome to criticize the proposal for the reasons you give, but the tone needs to be consistent with the code of conduct. This means not attacking the authors of proposals, and not using hyperbolic (and politically charged) analogies.

11 Likes

As long as that difficulty isn't resolved, the proposal isn't useful at all. So having it in "future directions" wouldn't be enough.

Also, I don't see anything in "future directions" that would actually resolve it, do you mind pointing me towards what you were talking about?

Sure, silence me if you need to, but everybody that takes an actual look at this will see that in terms of what would happen if this proposal would be accepted, the only difference is that they replaced the opaque keyword with some. It's exactly the same otherwise. Exactly as bad as it used to be.

One of the future directions (at the very bottom of the proposal) is introducing any as a dual to some for explicitly spelling existential types.

Unless I am mistaken about the proposal (which I may well be), this would allow you to write something like

let d: [any A] = [b(), c()]

But any would be an existential type. If I need existential types to use my opaque types properly, then opaque types aren't useful alone. And if that's the case – if I'm going to end up using existentials anyways eventually in the code – why not just use existentials to begin with?

func b() -> any A {
    return ...
}

That's a pretty large generalization; opaque types are useful "alone" (without existential types), as the proposal demonstrates with many different use-cases. If you closely read the proposal, you will find that some is actually intended to be implemented as syntactic sugar for "reverse generics," as they are referred to in the proposal. Using some is not the same thing as using an existential type.

3 Likes

There are some holes, but I still think the feature is useful by itself. The important thing is that there is a path to eventually allow arrays and optionals of these types in the future.

We definitely should double check that we aren't painting ourselves into a corner...

I would personally like to see the ability to define one of these Opaque types in a typealias to be used within the entire type, and it would be known to be the same type throughout the type. Not sure how hard that would be on the type checker though.

If it was possible, it would also eventually allow these opaque types to be passed in parameters, which would open up a bunch more use-cases. Basically, a collection could say: I am going to give you an index, but all I am going to guarantee about it is that it has properties X,Y, & Z (e.g. it is Strideable). Then I could pass it back to a function with the guarantee that it is the same type (even though I don't know type that was).

struct MyType {
    typealias PartId = some CustomStringConvertable

    func part(under point: CGPoint) -> PartId
    func selectPart(_ id:PartId)
}

Doesn't need to be in this version, but would be nice to know if that would eventually be possible. I can imagine that this might put strain on the type checker though if we don't have a way to give it a hint about what that type should be. Maybe you have to define the actual type as part of the typealias, but all that is exposed outside of the typealias is the opaque type...

Regarding composability, I suggested earlier that we need a way to name the opaque type to fix this. Apparently it "would be easy to add". Beside solving the comparability problem, I believe it would also be a good teaching tool.

It'd still be a nice gesture to see this acknowledged in future directions. It would indicate the author of the proposal understands there is a potential issue there and that it can be solved, even if only in a hypothetical future. It was a concern for more than one people in the previous review thread after all, so I too would have expected to see a mention of it.

I also note the "UI for generics" document is silent on this topic. I find it's a bit like looking very far ahead while missing the immediate issue right under our nose.

That said, adding a way to express the type name is something that can be fixed later. If it takes some pain to discover we need a way to name the type, then it's unfortunate but so be it.

The section on Opaque type aliases has this to say:

This would be a great feature to explore as a future direction, since it has some important benefits relative to opaque result types.

One can argue that this should be a part of Future Directions, but one cannot make the case that it's not addressed in the proposal.

1 Like

If you can change the original function and replace the anonymous opaque type with an opaque typealias, then great! But returning an anonymous opaque type you can't refer to (by name or otherwise) is still a composability problem the API writer can inflige on its users.

I don't see how this is functionally any different than a function which returns a custom type. It can't compose with other functions because no other function returns the same type. You're arguing that when the compiler knows (or can be made to know) that 2 functions do return the same type, it should allow composition. I don't think anyone disagrees with this.

One may also argue that opaque signatures which are spelled the same are going to appear to be the same, even though they aren't. That may also be a concern, but I don't think you can get away from that, as it's a fundamental aspect of the feature.

From my comment in the previous review thread:

If you can't name it, it's difficult to store as a variable in a struct or elsewhere, and it's difficult to write wrapper functions without each of them creating a new distinct opaque type.

In other words, when using functions returning opaque types, you now have an inability to write convenience wrapper functions that return the same type:

// module A, maintained by X
func makeShape(params: [Any: Any]) -> some Shape { ... }
// module B, maintained by Y
func makeCircle() -> some Shape {
  return makeObject(params: ["type": "circle"])
}
func makeSquare() -> some Shape {
  return makeObject(params: ["type": "circle"])
}
makeCircle() == makeSquare() // error: two distinct types
[makeCircle(), makeSquare()] // error: two distinct types

All maintainer Y can do is beg maintainer X to not use an anonymous type and use a named type (opaque or not). Either that or Y has to avoid writing helper functions of this sort.

There is no syntax to express the name and that makes the type more difficult to deal with in ways that didn't exist before in the language. So it is definitely different than a function which returns a custom type today.

2 Likes

I agree. But this proposal is adding a feature that doesn't exist today. No one will force developers to use opaque types before they allow more convenience for downstream developers. It lays the groundwork for such improvements, while also giving a feature that is useful in limited contexts. One could make the case that having a half-baked feature will cause short-term harm, but that's very different than Vogel's assertion that such harm is endemic to the feature.