SE-0198 — Playground QuickLook API Revamp

Hi Swift Community,

The review of SE-0198 “Playground QuickLook API Revamp” begins now and runs through February 8th, 2018.

Reviews are an important part of the Swift evolution process. All reviews should be made in this thread on the Swift forums or, if you would like to keep your feedback private, directly in email to me as the review manager.

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

What is your evaluation of the proposal?
Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
More information about the Swift evolution process is available on the Swift evolution website2.

As always, thank you for participating in Swift Evolution.

Ben Cohen
Review Manager

3 Likes

As a heavy user, I really don't like the churn (whose timing could not be worse for me, honestly) but I like the outcome. It makes sense to move things out from the stdlib now but I wish this had been done a year ago.

  • I wouldn't describe CustomPlaygroundDisplayConvertible as an "alternative description" in the official proposal text but rather as an alternative depiction, as many custom uses are graphical in nature (.image, .bezierPath, .view, .sprite. I've never gotten sound to work but I've not tried it for a fairly long time.)

  • Conforming with self is a great shortcut/placeholder.

  • I super-like that a struct can return an array of CustomPlaygroundDisplayConvertible-conforming instances, so I can return a picture and an attributed string, which I'm hoping will be presented in array order within the value history box, allowing me to omit attachment attribute overhead. I'd like if the proposal clarified the exact nature of this "inheritance" with an example.

  • Nearly all use of custom quicklooks are in special purpose playgrounds used to demonstrate or educate so there will a cost to hand migration. Still, I don't think the feature is used enough in the overall community to require migration. I'm not looking forward to it but I know I'll survive.

So, suppose I write a package with some types in it and I want to customize those types’ appearance when someone happens to use them in a playground. How should I do this? Do I import PlaygroundSupport and conform to the protocol? Is the framework available and public on all Swift platforms? Does that add overhead for the many uses that will be in apps or other non-playground environments?

1 Like

I'm not exactly sure what this means, so I want to clarify. Returning self from the implementation of playgroundDescription is at the very least discouraged, and may be treated as an error by PlaygroundLogger. (This is due to the chaining nature of CustomPlaygroundDisplayConvertible; PlaygroundLogger would see that the returned value also conforms to CustomPlaygroundDisplayConvertible and retry with that, which would just keep happening until the stack runs out of space or PlaygroundLogger detects this and treats it as an error.)

If you return an array from playgroundDescription, then it will be treated as an array when being logged. The children of the array will generate opaque log entries but the array would effectively be logged as [Any], not a special multi-representation value.

That's why there will be a special, playground-only shim library which continues to provide PlaygroundQuickLook and CustomPlaygroundQuickLookable in their deprecated form to Swift 3.x/4.x playgrounds. Do you think this deprecation approach gives sufficient time to perform the manual migration (which should be fairly straightforward in each individual case, at least)?

This proposal does represent a decrease in availability of CustomPlaygroundDisplayConvertible as compared to CustomPlaygroundQuickLookable. The PlaygroundSupport framework is only available to playgrounds and their auxiliary sources, so adding a conformance to this protocol must be done in a playground or its auxiliary sources. PlaygroundSupport is not generally available for import into projects/packages.

There are a handful of reasons behind this decision:

  • It feels weird for the Swift standard library to know about and vend API for such an Xcode-specific feature
  • A manual search of GitHub suggested that CustomPlaygroundQuickLookable was basically unused outside of playgrounds, and an earlier manual check of the compatibility suite found no uses of CustomPlaygroundQuickLookable (I'm going to start a run against the removal PR later today to confirm that's the case)
  • By putting CustomPlaygroundDisplayConvertible in PlaygroundSupport today, we're able to move it to the standard library (or a similarly more available library) in a future Swift version without breaking source compatibility. (Binary compatibility is not a concern for PlaygroundSupport, as it is only imported in always-compiled-from-source playgrounds.) However, if we put CustomPlaygroundDisplayConvertible into the standard library today, we'd be stuck with it there forever.

There's also an admittedly not great workaround to this limitation: since CustomPlaygroundDisplayConvertible doesn't reference any custom types, a project/package could include an implementation of playgroundDescription, and then a playground could retroactively add a conformance to CustomPlaygroundDisplayConvertible.

1 Like

This is what canImport is for:

#if canImport(PlaygroundSupport)

  import PlaygroundSupport

  extension MyStruct: CustomPlaygroundDisplayConvertible {
    var playgroundDescription: Any {
      return "A description of this MyStruct instance"
    }
  }

#endif

This code will only be compiled when PlaygroundSupport is available for the platform.

1 Like

Unfortunately, that's not quite right. PlaygroundSupport isn't generally available on any platform. It's only available as an import in the main playground code and any auxiliary source files included in that playground. So this #if would either always be disabled (because it's in a project/package and thus can't see PlaygroundSupport) or always be enabled (because it's in a playground).

So that means I can't distribute a compiled framework with built-in support for Playgrounds, doesn't it? Users will always need to copy the source files in.

BUT - in that case, you could still use canImport.

Conforming with self is a great shortcut/placeholder.

I’m not exactly sure what this means, so I want to clarify. Returning self from the implementation of playgroundDescription is at the very least discouraged, and may be treated as an error by PlaygroundLogger. (This is due to the chaining nature of CustomPlaygroundDisplayConvertible; PlaygroundLogger would see that the returned value also conforms to CustomPlaygroundDisplayConvertible and retry with that, which would just keep happening until the stack runs out of space or PlaygroundLogger detects this and treats it as an error.)

You exclude the possibility, saying "Although that capability is no longer present, in most cases implementors of CustomPlaygroundDisplayConvertible may return a custom description which closely mirrors their default description. One big exception to this are classes which are considered core types, such as NSView and UIView, as one level of subclass may wish to customize its description while deeper level may wish to use the default description (which is currently a rendered image of the view). This proposal does not permit that". I'd like to be able to return self and have it conform as a core type until time permits to customize. Would it be dreadfully hard to detect and do that?

If you return an array from playgroundDescription, then it will be treated as an array when being logged. The children of the array will generate opaque log entries but the array would effectively be logged as [Any], not a special multi-representation value.

Does that mean that a member, which is treated as Any & CustomPlaygroundDisplayConvertible will not show each member's inherited depiction?

Do you think this deprecation approach gives sufficient time to perform the manual migration (which should be fairly straightforward in each individual case, at least)?

No, I don't. That's because the product cannot be recalled for re-issue or updated through any normal "this playground is out of date. update?" procedure. I'd honestly prefer you just break things now and skip the deprecation.

What about, eg. “self as UIView”?

Correct, if you share source files between a compiled framework and a playground then canImport is a great solution.

Unfortunately, in the general case, it is. If self is an object, then it's trivial to detect as you can use ===. However, if self is a value type, then === is not available, and to my knowledge there's no way to detect that.

In "Alternatives considered", I supplied a possible alternative which would let this work generally. I didn't include a code snippet but imagine that PlaygroundSupport provided a DefaultPlaygroundDisplay<T>:

extension MyType: CustomPlaygroundDisplayConvertible {
    var playgroundDescription: Any {
        return DefaultPlaygroundDisplay(self)
    }
}

I didn't include that in this proposal, as it seems like an enhancement which can come later. I'll turn this around: if a type is already a "core type" (either directly or via subclassing), then it's not required that you override CustomPlaygroundDisplayConvertible to get it to display as that "core type". What's the benefit to being able to include an empty implementation of CustomPlaygroundDisplayConvertible before it wants to be customized? (Do you frequently implement CustomStringConvertible but have it return the "default" value, knowing you'll come back to it later?)

Not quite sure I understand the question -- the rules in general for playground logging aren't changing as part of this proposal. To put it in PlaygroundLogger terms, if you return an array containing an NSLocalizedString and an NSColor, it'll generate:

LogEntry.structured(…, children: [
    LogEntry.opaque(…, payload: .attributedString(<data>)),
    LogEntry.opaque(…, payload: .color(<data>))
])

The intent is that both foo and bar in the example below should produce identical playground displays:

let attrString: NSAttributedString = //...
let color: NSColor = // ...

extension MyType: CustomPlaygroundDisplayConvertible {
    var playgroundDescription: Any {
        return [attrStr, color]
    }
}

let foo = MyType()
let bar = [attrStr, color]

OK, thanks for the feedback. (And I'm sorry for the churn -- but with ABI stability around the corner, we want to kill this actively harmful API and replace it with something better, hence this proposal.)

Unfortunately, this doesn't work -- in an Any, self and self as UIView are identical because the Any is wrapping the same object in both cases. So when PlaygroundLogger comes along and tries to log that Any, it has no way of knowing that the source code tried to specify that it should be logged as a UIView.

What does this mean for Playgrounds which I create as internal documentation for my iOS/macOS projects?

Let's say I have an iOS app, with 2 modules: MyModelKit and MyApp. When I create these internal Playgrounds, I usually have to import MyModelKit, because Xcode uses the already-compiled product when executing the Playground, rather than including my entire project as source.

So does this mean I won't be able to add custom Playground representations of types in MyModelKit, because when that module is compiled, it can't see PlaygroundSupport?

If so, that would be an unacceptable limitation for me and I'd have to -1 the proposal.

The stuff I need to provide a custom playground representation needs to be available in all contexts. Maybe it should go in a different library, and not PlaygroundSupport? Or maybe PS can hide all of its irrelevant symbols in non-Playground contexts?

Yes, correct. (Though as noted above, there would be a hacky workaround -- implement playgroundDescription in the core framework, and then retroactively declare conformance to the protocol in the auxiliary sources for your playgrounds.)

That's not really possible, unfortunately, because PlaygroundSupport is provided by the IDE or a custom toolchain. It's not available at runtime outside of a playground context, so it wouldn't be possible to make it available at compile time. (It's kind of a similar situation as XCTest; it's not available for use in regular app/framework code because the framework isn't part of the OS, but the framework is available for use in specific contexts.)

The review of SE-0198 has ended, and the Core Team has accepted the proposal with minor revision:

The new API for customizing playground display will be adopted, and the previous API deprecated, as described in the proposal. However, the new protocol will remain part of the standard library rather than moving to the playground support library, in order to facilitate frameworks that want to ship with built-in custom playground display capability. @cwakamo will update the proposal accordingly.

Thank you to everyone who participated in the review!

Ben

Having this remain part of the standard library is a pretty big scaling-back of the proposal, and I mean, in general this shouldn't be necessary, right? We should be able to support these kinds of use-cases. Not everybody can become part of the standard library.

How about if PlaygroundSupport (or some subset of it) was available at compile-time? If there is no cross-module inlining, it should be used in a header-only capacity. So when I compile my library, the compiler should be able to generate a conformance to CustomPlaygroundDisplayConvertible in the same way that I can throw some header files at a C compiler and start using symbols from it.

The issue, as you say, is at runtime. The IDE provides its own implementation of PlaygroundSupport which generally isn't suitable for use in any other Apps. This is a dynamic linking problem. The linker should not be finding the IDE's private implementation of the library, and any Apps which try to use it should fail (at least at runtime). We could add a special check to catch that at compile-time, but theoretically somebody might create an implementation which you could arbitrarily import from any context (there's a lot of interest around Playgrounds).

1 Like