[Pitch] Have all adapters in the standard library uniformly expose their `base`

For example, consider EnumeratedSequence. It is a shame that it doesn't conform to Collection when its Base does (that's a separate problem that should be fixed), but if its Base were exposed as a base property, I could add that conditional conformance retroactively. Unfortunately, it has no publicly accessible Base instance, so I can't. That's an indication that base is a missing piece.

5 Likes

At least for wrapper types that narrow/filter their base collection, adding a base property would feel like an abstraction violation to me. (A pragmatic one, but still -- I would not like to make it easy for a function that takes, say, a LazyFilterSequence to just call base and completely escape the abstraction behind my back.)

(I've been particularly annoyed at base properties on concrete slice types such as Substring.base and ArraySlice.base recently. I've seen little to no legitimate use cases for these so far, outside of the slice/collection implementations themselves.)

Clearly, this doesn't really apply to EnumeratedSequence itself. But even for that, instead of encouraging folks to add retroactive conditional conformances that would be all but guaranteed to become a liability later, I'd much prefer if we either trimmed back & revived SE-0312 (which ran into some trouble further down the collection hierarchy), or if we rather recommended/normalized building one's own custom type that is tailored to their specific needs.

7 Likes

FWIW, ArraySlice.base is not public. I even brought this up recently:

I've found the base property extremely helpful for "framing" a subsequence in its parent context. E.g. when parsing a substring, access to the base makes it easy to render an error that points to the start/end indices of the substring within the larger string. The same use case could be applied to array slices.

Is there a "better" way to achieve this kind of thing without reaching for the base sequence?

5 Likes

Ah, I forgot ArraySlice is the lucky one.

I agree it's very useful to be able to peer into / break abstractions for debugging purposes. It seems to me this sort of thing would be better done through reflection, though.

More generally, I'd really like to see indices and index ranges get printed the same way!
(Of course, that's not really possible without someone inferring the collection value that corresponds to them.)

Meh. These things all should have been exposed from the beginning. Every place we held back a constructor or an exposed base part, it has presented a practical obstacle to real coding problems. The adapters are utilities and abstractions, but were not intended to be a security mechanism. If you really want to make those kinds of specifics unavailable, you should be passing people some Sequence where Element == X ersump'n (and yes, that feature needs to be implemented). Look at it this way: the exposed type of a thing should be meaningful to its clients. If you hide the base of these adapters, they really are no different from some opaque Sequence or Collection, and should be presented as such, with some.

6 Likes

Fully agreed! These should be opaque.

2 Likes

Fully disagreed, then. That limits their usefulness. The person who wants to hide that information can always coerce them into the opaque wrapper type. The standard library should expose components with maximal utility. Otherwise, people will end up rewriting standard library components just to get access to the information the standard library components have, but won't expose. That is, in fact, exactly what I'm doing with Zip2Sequence right now. What a waste.

4 Likes

Well, I definitely don't think it would be a good idea to expose EnumeratedSequence._base to make it easier to conform this type to stdlib protocols outside of the stdlib. That's not maximum utility, that's just inviting future headaches, possibly even cutting off our ability to add this conformance to the stdlib later.

If it's a good idea for EnumeratedSequence to conditionally conform to Collection (which I do believe it clearly is), then the responsible thing to do is to implement that within the Standard Library, by picking up work on SE-0312.

(EnumeratedSequence is 83 lines of code, about half of which are doc comments. Duplicating it is definitely annoying, but I don't really see how it's a huge hardship...)

(FWIW, Stephen's use case is far more convincing to me.)

Edit: Apologies if I'm coming across as overly cranky here. Once we have the missing Collection conformance in the stdlib, adding base would become a lot more palatable to me. (I still don't think base should be a standard feature for all collection adapters, though.)

Hear, hear
  • I had to replicate quite large chunk of SwiftUI's Font / Color / Image machinery because some clever folks decided to make them overly opaque, so I'm not entitled to determine if Font is, say, bold [Font.body.bold()] or if image is standard photo [Image(systemName: "photo")], etc - no public getters to determine properties of the mentioned types.

  • Every swiftUI developer every now and then has to work around SwiftUI bugs omissions by accessing the underlying UIView, which is (of course) not exposed as a public property.

2 Likes

That's not why, of course; we are all suspicious of fully retroactive conformances. But it's what I need to do to fill practical need and is just one example of a problem that keeps coming up for me in various forms: the standard library fails to expose the maximum useful API.

And sure, in this particular case there's something the standard library should change so I don't have to do this. Of course, that's going to be pretty unsatisfying too until this problem is fixed (there's no reasonable way to make a Collection wrapper that conditionally conforms to BidirectionalCollection when its wrapped base does). And that in turn is waiting on a fix for the language semantics. That could be years in the making, if it ever comes. So, yeah, in the meantime I'd appreciate it if the library would at least not make useful information inaccessible.

( EnumeratedSequence is 83 lines of code, about half of which are doc comments. Duplicating it is definitely annoying, but I don't really see how it's a huge hardship...)

You've gotta be kiddin' me, brother! Extended to all the adapters in the standard library, that negates the whole point of libraries, to say nothing of the standard library.

Edit: Apologies if I'm coming across as overly cranky here.

Be as cranky as you like; I can definitely out-crank you. The sentiments you're expressing are in direct conflict with my original design intentions for the standard library, so not particularly easy to swallow. I do of course accept that I have no say over these things anymore, though…

6 Likes

It’s interesting to hear that even with the new some features people find that these types are useful; I definitely would not have exposed them if I were building the stdlib from scratch with the language we have now. Information hiding is of dubious value when you don’t have ABI boundaries, but when you do it’s the only thing standing between your codebase and complete calcification.

1 Like

The Swift standard library was never supposed to be an Apple Framework™ in the style of the ObjC frameworks of Olde, where you expect to be able to swap out the implementations and data layout of practically everything, whenever you want to. It's supposed to be super-efficient building blocks for those frameworks, and to do that, you accept a certain amount of ABI lock-in. We only tried to maintain resilience for those things where there clearly wasn't going to be a huge win from inlining. Certainly, these collection adapters are never going to not store a copy of their base collections, so the flexibility to change is not particularly useful in this case.

2 Likes

I think it’s easy to conflate the standard library with any other resilient library, especially system libraries. So I agree that the standard library has some implementation details that are really unlikely to change, and exposing them seems perfectly reasonable given that we’re talking about the fundamental building block of most Swift programs. I still think that SwiftUI rightfully remains opaque, because there have been many times were color/image encodings have changed or where Apple is working on something new behind the scenes they don’t want to expose. After a few years, it would probably be fair to request less opaqueness from basic SwiftUI currency types, and going by this principle it seems that the standard library is already here, as evident by every other function being inlineable.

Abstraction protects not only the implementation but also the interface. Surely at some point the STL has been forced to retain or permit a suboptimal usage pattern for the sake of API compatibility.

4 Likes

I'm wondering -- do these issues not apply to the conformances you're trying to implement outside the stdlib?

<bemused eyeroll>

Look, something has gotta give. You suggested to add EnumeratedSequence.base so that you can implement retroactive conformances on it outside the stdlib. I think that's a terrible reason -- bad enough to taint the API suggestion itself. If that's really what base is for, then I'd really rather we did not have it -- at least not until we've added the obviously missing conformances to the stdlib.

@stephencelis's use case feels more convincing to me in every way. Are there any other legitimate use cases for a base property?

To be very clear, I was not talking about any "security" concerns. That would be extremely silly.

Most of the ABI breaks I’ve wanted to do are for improved efficiency, is the thing. Heck, one of the biggest we ever did is the String rewrite, which was a massive perf win.

(edited to add)

Note that I’m not necessarily saying the chosen tradeoffs were incorrect in this case. Just that it’s not an obvious decision and the costs of both design approaches are steeper than I’d like. Cross-module inlining vs future maintenance and improvements may even be the most challenging dilemma the stdlib faces, though there’s a few other contenders.

6 Likes

That info should belong to the state, which you control and can read at any time. Also, seems like an edge-case, I'm now curious why did you need it?

As for the main debate here, fully opening an abstraction because some features are missing is not the best approach imo. I'd rather see those missing features implemented.

1 Like

You're assuming here that what I like about opaque result types is that they leave things flexible on the ABI level. In fact, what I actually find attractive about them for the particular case of lazy collection algorithms is their source-level opacity.

As a user of the language, I love that their interface is completely defined by their protocol conformances -- if I understand the protocol, I understand the type.

As a library author, I love that they get rid of the need to invent a workable public name for a result type that no one wants to remember or spell out. In practice, these collection transformations have a tendency to be chained, resulting in deeply nested A<B<C<D,E<F>,G<H,I>,J>,K>> types that aren't nice to work with at all. Opaque result types point us a way to cut through this mess by hiding it all. (Even if only superficially.)

On a more abstract level, I'd find it exciting to figure out how far we can take things within their constraints -- such as the need to describe a type entirely through its protocol conformances. Weird constraints sometimes lead to breakthroughs. (Then again, sometimes they just make things unnecessarily difficult, like trying to argue over the internet. ¯\_(ツ)_/¯)

The fact that in their current form opaque result types are also opaque on the ABI level is absolutely essential above a certain (not even that high!) level in the software stack, and it's clearly the primary reason we have them. As you so kindly explained, this particular aspect is completely irrelevant (and would be actively harmful) to most parts of the stdlib -- especially something as trivial as the enumerated() collection transformation. I agree with that, of course.

Note that performance isn't even the most fundamental objection -- opaque result types also have language-level limitations that (currently?) completely prevent their use in enumerated()-like functions. (For one thing, it'd be impossible to describe the conditional conformances that started this discussion in the first place.)

Still, I'd love it if we could figure out a way to use these ideas to build an efficient transformation library.

But to be honest, I don't really see how any of this has anything to do with base properties. (An opaque result type could provide that, too.)

7 Likes