[Proposal] Revamp the playground quicklook APIs

Well, in my experience performance issues tend to come not from trying to display a single object, but when you have an aggregation of many objects. For example, displaying one Int is pretty lightweight, as is an [Int] with a handful of elements, but displaying an [Int] with million elements is not (this is assuming that collections will run the playgroundRepresentation on their individual elements as they do currently). I don’t think the current proposal can solve this issue as it’s pitched currently. (For those curious, this doesn’t have a good solution today, either: the best “solution” I’ve been able to come up with is to move performance-sensitive out into a Playground’s Sources folder, which isn’t displayed.)

Fundamentally, I don’t think it’s clear what the “default” value is supposed to be: we already have three different interpretations of what it could do: do literally nothing (i.e. display an empty representation), use a superclass’s representation, or use a structural representation based on whether the type is an enum, struct, class, etc. I think we need to clear up what the default actually means (and if we even need it at all) before we can proceed.

Saagar Jha

···

On Jan 10, 2018, at 10:02, Chris Lattner <clattner@nondot.org> wrote:

On Jan 9, 2018, at 11:30 PM, Saagar Jha <saagar@saagarjha.com> wrote:

In short, can we change playgroundRepresentation to return Any instead of Any?. Among other things, doing so could ease the case of playground formatting Optional itself, which should presumably get a conditional conformance to this. :-)

I believe the rationale behind this was to provide a way to “opt-out” of a customized representation, although now that I think about it, what exactly does the default mean? In particular, what happens in this case?

If you want a type to display as nothing (e.g. to avoid the perf impact of formatting it) just make the playground representation be an empty string or something.

-Chris

What is the use-case for a type conforming to this protocol but returning nil? If there is a use case for that, why not have such an implementation return “self” instead?

Riley and Saagar answered this down-thread, but to confirm — returning nil would allow some instances of a type to use the “default” playground logging presentation while others use an alternate presentation instead.

Right, this gets back to the question: what is the use case for this? When would a type want to “sometimes” replace the default representation?

It seems to me that a type author either wants to take control of presentation or not. While I’m sure we could imagine some use case for the behavior you’re describing, is it big enough to make it worth complicating the API?

This isn’t handled by `return self` because, unless I’m mistaken, there’s no way to detect that from the caller’s side (e.g. with two `Any` values, I can’t do `self === self.playgroundRepresentation`).

Ok

In short, can we change playgroundRepresentation to return Any instead of Any?. Among other things, doing so could ease the case of playground formatting Optional itself, which should presumably get a conditional conformance to this. :-)

I don’t think we can change this to return `Any` instead of `Any?`. I think there are potentially cases where a developer might want to selectively opt-in to this behavior.

Which cases? How important are they?

I can think of a couple of cases where this could be useful.

The first is an enum. Extending Riley’s example from earlier in the thread:

  enum MyUnion {
    case string(String)
    case image(UIImage)
    case intPair(Int, Int)
    case none
  }

This enum might want to present the string and image cases as strings and images, but treat the intPair and none cases the “default” way for the enum. This is probably not the most compelling example as there is a workaround — return a second enum or other structured type from playgroundRepresentation — but that feels not great.

The second case, and the one I’m more worried about, is subclassing. If, for instance, you have the following:

  class FooView: UIView, CustomPlaygroundRepresentable {
    var playgroundRepresentation: Any {
      return “A string describing this view"
    }
  }

  class BarView: FooView {
    override var playgroundRepresentation: Any {
      // BarView instances wanted to be logged as themselves, but there’s no way to express that
      return ???
    }
  }

There’s nothing that BarView can do to ensure it gets logged like a view because FooView declared a conformance to CustomPlaygroundRepresentable.

I’m not sure how important these cases are. If it were just the first one, I’d probably say we can safely ignore it because there’s a reasonable workaround. But the subclassing case worries me because there’s no escape hatch (aside from a potential implementation-defined failsafe).

I also don’t think that `Optional` would get a conditional conformance to this. I’m not proposing that any standard library or corelibs types gain conformances to this protocol. Instead, it’s up to a playground logger (such as PlaygroundLogger in swift-xcode-playground-support <https://github.com/apple/swift-xcode-playground-support&gt;\) to recognize these types and handle them accordingly. The playground logger would look through the `Optional` so that this would effectively be true, but ideally the log data generated by a logger would indicate that it was wrapped by `Optional.some`.

Why not? I understand that that is how the old algorithm worked, but it contained a lot of special case hacks due to the state of Swift 1 :-). This is a chance to dissolve those away.

It’s a feature that Optional (and other standard library/corelibs/OS types) don’t conform to CustomPlaygroundRepresentable. In my mind, it’s the role of the PlaygroundLogger library to understand the types for which it wishes to generate an opaque representation instead of the standard/fallback structured representation. So Optional, String, Int, UIColor, NSView, etc. don’t themselves conform to CustomPlaygroundRepresentable — they’re not customizing their presentation in a playground.

Semi-related to this proposal, I’m working on a rewrite of the PlaygroundLogger library (currently at <https://github.com/cwakamo/swift-xcode-playground-support/tree/runtime-framework-and-improved-logger&gt;\) which makes it so that the only special standard library behavior it depends on is Mirror — it no longer relies on the Swift runtime (via PlaygroundQuickLook(reflecting:)) to figure out what to log, instead opting to check protocol conformances itself. So if this proposal is accepted into Swift, concurrent with that we’ll have a new PlaygroundLogger implementation which gets rid of as many hacks as possible.

To be clear, I would expect that the conformance for optional would be defined in the playground module along with this protocol - it wouldn’t be defined in the standard library itself.

One possibility would be to change the API so that it returns an enum. Imagine:

  enum PlaygroundLoggingBehavior {
    /// Asks the playground logger to generate the standard logging for `self`.
    case standard

    /// Asks the playground logger to generate logging for the given `Any` instead of `self`.
    case custom(Any)
  }

  protocol CustomPlaygroundLoggable {
    /// Returns the `PlaygroundLoggingBehavior` to use for `self`.
    var playgroundLoggingBehavior: PlaygroundLoggingBehavior { get }
  }

(To Saagar’s point in another email — you could even add a `case none` to PlaygroundLoggingBehavior to inhibit logging of a particular instance.)

`CustomPlaygroundLoggable` would be a little clunkier to implement than `CustomPlaygroundRepresentable` is, as in the common case folks would have to write `return .custom(…)`. It’s possible that the clarity and additional flexibility this grants outweighs that cost; I’m not sure, and would love feedback on that.

I just don’t understand the usecase for “conditional customizing” at all. By way of example, we don’t have the ability to do that with CustomStringConvertible. What is different about this case?

I think the big difference with CustomStringConvertible is that it’s possible for a conformance to reimplement the default behavior on its own. For instance, if I have:

  enum Foo {
    case one(Any)
    case two
  }

As noted above, recovering the default behavior with CustomPlaygroundRepresentable is not always possible if the return type is `Any`. That very well might be an acceptable trade-off to keep the API simple.

/// Implementors of `CustomPlaygroundRepresentable` may return a value of one of
/// the above types to also receive a specialized log representation.
/// Implementors may also return any other type, and playground logging will
/// generated structured logging for the returned value.
public protocol CustomPlaygroundRepresentable {

On the naming bikeshed, the closest analog to this feature is CustomStringConvertible, which is used when a type wants to customize the default conversion to string. As such, have you considered CustomPlaygroundConvertible for consistency with it?

The only prior art for the word “Representable” in the standard library is RawRepresentable, which is quite a different concept.

  /// Returns the custom playground representation for this instance, or nil if
  /// the default representation should be used.
  ///
  /// If this type has value semantics, the instance returned should be
  /// unaffected by subsequent mutations if possible.
  var playgroundRepresentation: Any? { get }

Again to align with CustomStringConvertible which has a ‘description’ member, it might make sense to name this member “playgroundDescription”.

I’m definitely open to different names for this. (`CustomPlaygroundRepresentable` was inspired by the API I’m removing, `CustomPlaygroundQuickLookable`, as they both take their sole property and make them -able.)

I do like the `playgroundDescription` name for the property, but am a little hesitant to use the name `CustomPlaygroundConvertible` because conforming types can’t be converted to playgrounds. I can’t come up with an appropriate word in `CustomPlaygroundThingConvertible` to use in place of `Thing`, though. (If we end up pivoting to the enum I described above then something like `CustomPlaygroundLoggable` would be more appropriate.)

I would strongly recommend aligning with the state of the art in CustomStringConvertible (which has been extensively discussed) and ignore the precedent in the existing playground logging stuff (which hasn’t).

That’s very reasonable. I’ll update the proposal to use CustomPlaygroundConvertible (unless I or someone else can come up with a really good “Thing” for a name like CustomPlaygroundThingConvertible, as that would even better match CustomStringConvertible/CustomDebugStringConvertible).

Connor

···

On Jan 10, 2018, at 4:21 PM, Chris Lattner <clattner@nondot.org> wrote:

On Jan 10, 2018, at 2:10 PM, Connor Wakamo <cwakamo@apple.com <mailto:cwakamo@apple.com>> wrote:

Well, in my experience performance issues tend to come not from trying to display a single object, but when you have an aggregation of many objects. For example, displaying one Int is pretty lightweight, as is an [Int] with a handful of elements, but displaying an [Int] with million elements is not (this is assuming that collections will run the playgroundRepresentation on their individual elements as they do currently). I don’t think the current proposal can solve this issue as it’s pitched currently. (For those curious, this doesn’t have a good solution today, either: the best “solution” I’ve been able to come up with is to move performance-sensitive out into a Playground’s Sources folder, which isn’t displayed.)

Yes, this proposal does not affect this case: the playground transform will still instrument all of the source code in the main source file. This proposal is merely about deprecating/removing a substandard API for controlling how values are presented in playgrounds with a better, more flexible one. (Though see my other reply to Chris where I present an alternative which could be extended to include support for disabling logging altogether for an instance.)

Fundamentally, I don’t think it’s clear what the “default” value is supposed to be: we already have three different interpretations of what it could do: do literally nothing (i.e. display an empty representation), use a superclass’s representation, or use a structural representation based on whether the type is an enum, struct, class, etc. I think we need to clear up what the default actually means (and if we even need it at all) before we can proceed.

This API is a bit wishy-washy as to what the “default” is. That’s somewhat intentional — the default presentation of an arbitrary Swift value/object is defined by the IDE, not by an API in Swift. I think my terminology is a bit confused here, and I’ll try to address that in a revision of the proposal. Let me try to clarify this a bit:

The fundamental design of playgrounds is that the compiler will insert calls to a logging function which is effectively required to take an `Any`. So therefore every instance of every type must be loggable by the playground logger. The PlaygroundLogger framework in swift-xcode-playground-support <https://github.com/apple/swift-xcode-playground-support&gt; implements this by generating either structured log entries using `Mirror` or specialized, opaque (aka IDERepr) log entries. PlaygroundLogger will generate a structured log entry for most instances, but for instances of special types it knows about (e.g. String, Int, NSColor, UIView, etc.) it will generate an opaque log entry which an IDE can then consume and display as is appropriate.

The CustomPlaygroundRepresentable API I’ve proposed does exactly one thing: it allows instances of conforming types to provide an optional stand-in to be used by the playground logger. So PlaygroundLogger would handle the following cases thusly:

  - If a type conforms to CustomPlaygroundRepresentable and returns a non-nil value, PlaygroundLogger will generate the appropriate log entry for the returned value
  - If a type conforms to CustomPlaygroundRepresentable and returns nil, PlaygroundLogger will generate the appropriate log entry for `self`
    - If `self` is one of the types (or a subclass of one of the types) for which PlaygroundLogger generates an opaque entry, PlaygroundLogger will generate an opaque log entry
    - Otherwise, PlaygroundLogger will generate a structured log entry

This process if potentially recursive (likely up to an implementation-defined limit): if `Foo: CustomPlaygroundRepresentable` and `Bar: CustomPlaygroundRepresentable`, and a `Foo` instance returns an instance of `Bar` from `playgroundRepresentation`, then the playground logger should effectively log `self.playgroundRepresentation.playgroundRepresentation`.

Connor

···

On Jan 10, 2018, at 12:39 PM, Saagar Jha <saagar@saagarjha.com> wrote:

CustomPlaygroundPreviewConvertible?

···

On Jan 11, 2018, at 11:22 AM, Connor Wakamo via swift-evolution <swift-evolution@swift.org> wrote:

That’s very reasonable. I’ll update the proposal to use CustomPlaygroundConvertible (unless I or someone else can come up with a really good “Thing” for a name like CustomPlaygroundThingConvertible, as that would even better match CustomStringConvertible/CustomDebugStringConvertible).

CustomPlaygroundRepresentationConvertible?

Saagar Jha

···

On Jan 11, 2018, at 11:22, Connor Wakamo via swift-evolution <swift-evolution@swift.org> wrote:

On Jan 10, 2018, at 4:21 PM, Chris Lattner <clattner@nondot.org <mailto:clattner@nondot.org>> wrote:

On Jan 10, 2018, at 2:10 PM, Connor Wakamo <cwakamo@apple.com <mailto:cwakamo@apple.com>> wrote:

What is the use-case for a type conforming to this protocol but returning nil? If there is a use case for that, why not have such an implementation return “self” instead?

Riley and Saagar answered this down-thread, but to confirm — returning nil would allow some instances of a type to use the “default” playground logging presentation while others use an alternate presentation instead.

Right, this gets back to the question: what is the use case for this? When would a type want to “sometimes” replace the default representation?

It seems to me that a type author either wants to take control of presentation or not. While I’m sure we could imagine some use case for the behavior you’re describing, is it big enough to make it worth complicating the API?

This isn’t handled by `return self` because, unless I’m mistaken, there’s no way to detect that from the caller’s side (e.g. with two `Any` values, I can’t do `self === self.playgroundRepresentation`).

Ok

In short, can we change playgroundRepresentation to return Any instead of Any?. Among other things, doing so could ease the case of playground formatting Optional itself, which should presumably get a conditional conformance to this. :-)

I don’t think we can change this to return `Any` instead of `Any?`. I think there are potentially cases where a developer might want to selectively opt-in to this behavior.

Which cases? How important are they?

I can think of a couple of cases where this could be useful.

The first is an enum. Extending Riley’s example from earlier in the thread:

  enum MyUnion {
    case string(String)
    case image(UIImage)
    case intPair(Int, Int)
    case none
  }

This enum might want to present the string and image cases as strings and images, but treat the intPair and none cases the “default” way for the enum. This is probably not the most compelling example as there is a workaround — return a second enum or other structured type from playgroundRepresentation — but that feels not great.

The second case, and the one I’m more worried about, is subclassing. If, for instance, you have the following:

  class FooView: UIView, CustomPlaygroundRepresentable {
    var playgroundRepresentation: Any {
      return “A string describing this view"
    }
  }

  class BarView: FooView {
    override var playgroundRepresentation: Any {
      // BarView instances wanted to be logged as themselves, but there’s no way to express that
      return ???
    }
  }

There’s nothing that BarView can do to ensure it gets logged like a view because FooView declared a conformance to CustomPlaygroundRepresentable.

I’m not sure how important these cases are. If it were just the first one, I’d probably say we can safely ignore it because there’s a reasonable workaround. But the subclassing case worries me because there’s no escape hatch (aside from a potential implementation-defined failsafe).

I also don’t think that `Optional` would get a conditional conformance to this. I’m not proposing that any standard library or corelibs types gain conformances to this protocol. Instead, it’s up to a playground logger (such as PlaygroundLogger in swift-xcode-playground-support <https://github.com/apple/swift-xcode-playground-support&gt;\) to recognize these types and handle them accordingly. The playground logger would look through the `Optional` so that this would effectively be true, but ideally the log data generated by a logger would indicate that it was wrapped by `Optional.some`.

Why not? I understand that that is how the old algorithm worked, but it contained a lot of special case hacks due to the state of Swift 1 :-). This is a chance to dissolve those away.

It’s a feature that Optional (and other standard library/corelibs/OS types) don’t conform to CustomPlaygroundRepresentable. In my mind, it’s the role of the PlaygroundLogger library to understand the types for which it wishes to generate an opaque representation instead of the standard/fallback structured representation. So Optional, String, Int, UIColor, NSView, etc. don’t themselves conform to CustomPlaygroundRepresentable — they’re not customizing their presentation in a playground.

Semi-related to this proposal, I’m working on a rewrite of the PlaygroundLogger library (currently at <https://github.com/cwakamo/swift-xcode-playground-support/tree/runtime-framework-and-improved-logger&gt;\) which makes it so that the only special standard library behavior it depends on is Mirror — it no longer relies on the Swift runtime (via PlaygroundQuickLook(reflecting:)) to figure out what to log, instead opting to check protocol conformances itself. So if this proposal is accepted into Swift, concurrent with that we’ll have a new PlaygroundLogger implementation which gets rid of as many hacks as possible.

To be clear, I would expect that the conformance for optional would be defined in the playground module along with this protocol - it wouldn’t be defined in the standard library itself.

One possibility would be to change the API so that it returns an enum. Imagine:

  enum PlaygroundLoggingBehavior {
    /// Asks the playground logger to generate the standard logging for `self`.
    case standard

    /// Asks the playground logger to generate logging for the given `Any` instead of `self`.
    case custom(Any)
  }

  protocol CustomPlaygroundLoggable {
    /// Returns the `PlaygroundLoggingBehavior` to use for `self`.
    var playgroundLoggingBehavior: PlaygroundLoggingBehavior { get }
  }

(To Saagar’s point in another email — you could even add a `case none` to PlaygroundLoggingBehavior to inhibit logging of a particular instance.)

`CustomPlaygroundLoggable` would be a little clunkier to implement than `CustomPlaygroundRepresentable` is, as in the common case folks would have to write `return .custom(…)`. It’s possible that the clarity and additional flexibility this grants outweighs that cost; I’m not sure, and would love feedback on that.

I just don’t understand the usecase for “conditional customizing” at all. By way of example, we don’t have the ability to do that with CustomStringConvertible. What is different about this case?

I think the big difference with CustomStringConvertible is that it’s possible for a conformance to reimplement the default behavior on its own. For instance, if I have:

  enum Foo {
    case one(Any)
    case two
  }

As noted above, recovering the default behavior with CustomPlaygroundRepresentable is not always possible if the return type is `Any`. That very well might be an acceptable trade-off to keep the API simple.

/// Implementors of `CustomPlaygroundRepresentable` may return a value of one of
/// the above types to also receive a specialized log representation.
/// Implementors may also return any other type, and playground logging will
/// generated structured logging for the returned value.
public protocol CustomPlaygroundRepresentable {

On the naming bikeshed, the closest analog to this feature is CustomStringConvertible, which is used when a type wants to customize the default conversion to string. As such, have you considered CustomPlaygroundConvertible for consistency with it?

The only prior art for the word “Representable” in the standard library is RawRepresentable, which is quite a different concept.

  /// Returns the custom playground representation for this instance, or nil if
  /// the default representation should be used.
  ///
  /// If this type has value semantics, the instance returned should be
  /// unaffected by subsequent mutations if possible.
  var playgroundRepresentation: Any? { get }

Again to align with CustomStringConvertible which has a ‘description’ member, it might make sense to name this member “playgroundDescription”.

I’m definitely open to different names for this. (`CustomPlaygroundRepresentable` was inspired by the API I’m removing, `CustomPlaygroundQuickLookable`, as they both take their sole property and make them -able.)

I do like the `playgroundDescription` name for the property, but am a little hesitant to use the name `CustomPlaygroundConvertible` because conforming types can’t be converted to playgrounds. I can’t come up with an appropriate word in `CustomPlaygroundThingConvertible` to use in place of `Thing`, though. (If we end up pivoting to the enum I described above then something like `CustomPlaygroundLoggable` would be more appropriate.)

I would strongly recommend aligning with the state of the art in CustomStringConvertible (which has been extensively discussed) and ignore the precedent in the existing playground logging stuff (which hasn’t).

That’s very reasonable. I’ll update the proposal to use CustomPlaygroundConvertible (unless I or someone else can come up with a really good “Thing” for a name like CustomPlaygroundThingConvertible, as that would even better match CustomStringConvertible/CustomDebugStringConvertible).

Connor
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

I don’t think we can change this to return `Any` instead of `Any?`. I think there are potentially cases where a developer might want to selectively opt-in to this behavior.

Which cases? How important are they?

I can think of a couple of cases where this could be useful.

The first is an enum. Extending Riley’s example from earlier in the thread:

  enum MyUnion {
    case string(String)
    case image(UIImage)
    case intPair(Int, Int)
    case none
  }

This enum might want to present the string and image cases as strings and images, but treat the intPair and none cases the “default” way for the enum. This is probably not the most compelling example as there is a workaround — return a second enum or other structured type from playgroundRepresentation — but that feels not great.

IMO, this is sugaring something that doesn’t need to be sugared. There are simple solutions to this problem without complicating the design of your feature. The few people who care about this can write it out long hand.

The second case, and the one I’m more worried about, is subclassing. If, for instance, you have the following:

  class FooView: UIView, CustomPlaygroundRepresentable {
    var playgroundRepresentation: Any {
      return “A string describing this view"
    }
  }

  class BarView: FooView {
    override var playgroundRepresentation: Any {
      // BarView instances wanted to be logged as themselves, but there’s no way to express that
      return ???
    }
  }

There’s nothing that BarView can do to ensure it gets logged like a view because FooView declared a conformance to CustomPlaygroundRepresentable.

I really don’t understand this. FooView declares that it conforms, and it provides a “playgroundRepresentation” member. Cool for FooView.

BarView comes around and overrides FooView’s implementation. They don’t have to conform, because it *isa* FooView, so of course it conforms. If it wants to customize its presentation, it overrides its “playgroundRepresentation” method and it… just works. If it conditionally wants the FooView representation for some reason, it can even call “super.playgroundRepresentation”. What’s the problem? This seems ideal to me.

I also don’t think that `Optional` would get a conditional conformance to this. I’m not proposing that any standard library or corelibs types gain conformances to this protocol. Instead, it’s up to a playground logger (such as PlaygroundLogger in swift-xcode-playground-support <https://github.com/apple/swift-xcode-playground-support&gt;\) to recognize these types and handle them accordingly. The playground logger would look through the `Optional` so that this would effectively be true, but ideally the log data generated by a logger would indicate that it was wrapped by `Optional.some`.

Why not? I understand that that is how the old algorithm worked, but it contained a lot of special case hacks due to the state of Swift 1 :-). This is a chance to dissolve those away.

It’s a feature that Optional (and other standard library/corelibs/OS types) don’t conform to CustomPlaygroundRepresentable. In my mind, it’s the role of the PlaygroundLogger library to understand the types for which it wishes to generate an opaque representation instead of the standard/fallback structured representation. So Optional, String, Int, UIColor, NSView, etc. don’t themselves conform to CustomPlaygroundRepresentable — they’re not customizing their presentation in a playground.

IMO, this was the right Swift 1 attitude: "make it work at all costs" but this is not the right approach for Swift over the long term, and not the right answer for Swift 5 in particular.

Swift is still very young and immature in some ways, but it it is intentionally design for extensibility and to be used in surprising and delightful ways. It isn’t an accident of history that Int and Bool are defined in the standard library instead of being burned into the compiler.

IMO, every time you choose to privilege “well known” types in the standard library with special code in Xcode (or some other high level system loosely integrated with swift.org) you are introducing technical debt into the Swift ecosystem. This is because you are violating the basic principles on which Swift was established, which is that users can [re]define primitives to build their own abstractions and carve out new domains beyond what was originally envisioned when you’re writing the UI code.

Semi-related to this proposal, I’m working on a rewrite of the PlaygroundLogger library (currently at <https://github.com/cwakamo/swift-xcode-playground-support/tree/runtime-framework-and-improved-logger&gt;\) which makes it so that the only special standard library behavior it depends on is Mirror — it no longer relies on the Swift runtime (via PlaygroundQuickLook(reflecting:)) to figure out what to log, instead opting to check protocol conformances itself. So if this proposal is accepted into Swift, concurrent with that we’ll have a new PlaygroundLogger implementation which gets rid of as many hacks as possible.

****awesome****

I hope that someday when we have a better API than Mirror (e.g. that supports mutation) that allows reflecting on stored properties, methods, and all of the other things that Swift needs to eventually support that you’ll switch to it. I understand that the glorious future isn’t very helpful to you today though.

`CustomPlaygroundLoggable` would be a little clunkier to implement than `CustomPlaygroundRepresentable` is, as in the common case folks would have to write `return .custom(…)`. It’s possible that the clarity and additional flexibility this grants outweighs that cost; I’m not sure, and would love feedback on that.

I just don’t understand the usecase for “conditional customizing” at all. By way of example, we don’t have the ability to do that with CustomStringConvertible. What is different about this case?

I think the big difference with CustomStringConvertible is that it’s possible for a conformance to reimplement the default behavior on its own. For instance, if I have:

  enum Foo {
    case one(Any)
    case two
  }

As noted above, recovering the default behavior with CustomPlaygroundRepresentable is not always possible if the return type is `Any`. That very well might be an acceptable trade-off to keep the API simple.

Why can’t an implementation just return “.two” instead of nil?

Is the problem the “one” case? If that is the problem, then it might be better to take a completely different approach where you embrace the fact that you have structured data producing 2D results that need to be displayed. Each result could either return an atomic result or a result that wraps some other recursive 2D presentation.

Fundamentally though, playground presentation is solving several different problems:

1. Types with no special behavior can always be represented as strings, which is handled by base protoocols.
2. Some types want custom string representations to show up in playgrounds, but not in “print” and string interpolation.
3. Some types want to provide a 2D “quicklook” style presentation in addition and beyond the string representation.
4. Fewer types want to provide an animated 2d representation, that is either “live” or precomputed.

I’d suggest that this protocol only tackle problems 2 and 3, but you should cleanly distinguish between them, probably with a bespoke enum or struct that models these capabilities.

I do like the `playgroundDescription` name for the property, but am a little hesitant to use the name `CustomPlaygroundConvertible` because conforming types can’t be converted to playgrounds. I can’t come up with an appropriate word in `CustomPlaygroundThingConvertible` to use in place of `Thing`, though. (If we end up pivoting to the enum I described above then something like `CustomPlaygroundLoggable` would be more appropriate.)

I would strongly recommend aligning with the state of the art in CustomStringConvertible (which has been extensively discussed) and ignore the precedent in the existing playground logging stuff (which hasn’t).

That’s very reasonable. I’ll update the proposal to use CustomPlaygroundConvertible (unless I or someone else can come up with a really good “Thing” for a name like CustomPlaygroundThingConvertible, as that would even better match CustomStringConvertible/CustomDebugStringConvertible).

Thank you!

-Chris

···

On Jan 11, 2018, at 11:22 AM, Connor Wakamo <cwakamo@apple.com> wrote:

Saagar Jha

Well, in my experience performance issues tend to come not from trying to display a single object, but when you have an aggregation of many objects. For example, displaying one Int is pretty lightweight, as is an [Int] with a handful of elements, but displaying an [Int] with million elements is not (this is assuming that collections will run the playgroundRepresentation on their individual elements as they do currently). I don’t think the current proposal can solve this issue as it’s pitched currently. (For those curious, this doesn’t have a good solution today, either: the best “solution” I’ve been able to come up with is to move performance-sensitive out into a Playground’s Sources folder, which isn’t displayed.)

Yes, this proposal does not affect this case: the playground transform will still instrument all of the source code in the main source file. This proposal is merely about deprecating/removing a substandard API for controlling how values are presented in playgrounds with a better, more flexible one. (Though see my other reply to Chris where I present an alternative which could be extended to include support for disabling logging altogether for an instance.)

Fundamentally, I don’t think it’s clear what the “default” value is supposed to be: we already have three different interpretations of what it could do: do literally nothing (i.e. display an empty representation), use a superclass’s representation, or use a structural representation based on whether the type is an enum, struct, class, etc. I think we need to clear up what the default actually means (and if we even need it at all) before we can proceed.

This API is a bit wishy-washy as to what the “default” is. That’s somewhat intentional — the default presentation of an arbitrary Swift value/object is defined by the IDE, not by an API in Swift. I think my terminology is a bit confused here, and I’ll try to address that in a revision of the proposal. Let me try to clarify this a bit:

The fundamental design of playgrounds is that the compiler will insert calls to a logging function which is effectively required to take an `Any`. So therefore every instance of every type must be loggable by the playground logger. The PlaygroundLogger framework in swift-xcode-playground-support <https://github.com/apple/swift-xcode-playground-support&gt; implements this by generating either structured log entries using `Mirror` or specialized, opaque (aka IDERepr) log entries. PlaygroundLogger will generate a structured log entry for most instances, but for instances of special types it knows about (e.g. String, Int, NSColor, UIView, etc.) it will generate an opaque log entry which an IDE can then consume and display as is appropriate.

The CustomPlaygroundRepresentable API I’ve proposed does exactly one thing: it allows instances of conforming types to provide an optional stand-in to be used by the playground logger. So PlaygroundLogger would handle the following cases thusly:

  - If a type conforms to CustomPlaygroundRepresentable and returns a non-nil value, PlaygroundLogger will generate the appropriate log entry for the returned value
  - If a type conforms to CustomPlaygroundRepresentable and returns nil, PlaygroundLogger will generate the appropriate log entry for `self`
    - If `self` is one of the types (or a subclass of one of the types) for which PlaygroundLogger generates an opaque entry, PlaygroundLogger will generate an opaque log entry
    - Otherwise, PlaygroundLogger will generate a structured log entry

Just wanted to make it totally clear here: is the superclass selected the one that is the closest to the current type, or the one that is the most distant parent? Referring back to the example I had further upthread:

class FooView: UIView {
  override var playgroundRepresentation: Any? {
    return “foo”
  }
}

class BarView: FooView {
  override var playgroundRepresentation: Any? {
    return nil
  }
}

Does BarView show up as a UIView, or a FooView? If it’s shown as a FooView, then returning nil is superfluous: you can just not override playgroundRepresentation and it’ll pick up the one from FooView.

···

On Jan 10, 2018, at 14:10, Connor Wakamo <cwakamo@apple.com> wrote:

On Jan 10, 2018, at 12:39 PM, Saagar Jha <saagar@saagarjha.com <mailto:saagar@saagarjha.com>> wrote:

This process if potentially recursive (likely up to an implementation-defined limit): if `Foo: CustomPlaygroundRepresentable` and `Bar: CustomPlaygroundRepresentable`, and a `Foo` instance returns an instance of `Bar` from `playgroundRepresentation`, then the playground logger should effectively log `self.playgroundRepresentation.playgroundRepresentation`.

Connor

I don’t think we can change this to return `Any` instead of `Any?`. I think there are potentially cases where a developer might want to selectively opt-in to this behavior.

Which cases? How important are they?

I can think of a couple of cases where this could be useful.

The first is an enum. Extending Riley’s example from earlier in the thread:

  enum MyUnion {
    case string(String)
    case image(UIImage)
    case intPair(Int, Int)
    case none
  }

This enum might want to present the string and image cases as strings and images, but treat the intPair and none cases the “default” way for the enum. This is probably not the most compelling example as there is a workaround — return a second enum or other structured type from playgroundRepresentation — but that feels not great.

IMO, this is sugaring something that doesn’t need to be sugared. There are simple solutions to this problem without complicating the design of your feature. The few people who care about this can write it out long hand.

Agreed.

The second case, and the one I’m more worried about, is subclassing. If, for instance, you have the following:

  class FooView: UIView, CustomPlaygroundRepresentable {
    var playgroundRepresentation: Any {
      return “A string describing this view"
    }
  }

  class BarView: FooView {
    override var playgroundRepresentation: Any {
      // BarView instances wanted to be logged as themselves, but there’s no way to express that
      return ???
    }
  }

There’s nothing that BarView can do to ensure it gets logged like a view because FooView declared a conformance to CustomPlaygroundRepresentable.

I really don’t understand this. FooView declares that it conforms, and it provides a “playgroundRepresentation” member. Cool for FooView.

BarView comes around and overrides FooView’s implementation. They don’t have to conform, because it *isa* FooView, so of course it conforms. If it wants to customize its presentation, it overrides its “playgroundRepresentation” method and it… just works. If it conditionally wants the FooView representation for some reason, it can even call “super.playgroundRepresentation”. What’s the problem? This seems ideal to me.

The issue is that there’s no way for `BarView` to recover the default playground representation which `UIView` and its subclasses get. For a `UIView`, the PlaygroundLogger library renders an image of the view and packages that up in a log entry. If `BarView` wants that instead of `FooView`’s custom representation, there’s no way for it to request it.

That being said, I think I’m convinced that this is enough of an edge case that shouldn’t complicate the design of the protocol. So in my updated proposal I’ll be going with:

  protocol CustomPlaygroundConvertible {
    var playgroundDescription: Any { get }
  }

If we find out that this isn’t as much of an edge case as thought, we can add a type like `DefaultPlaygroundRepresentation<Wrapped>` to PlaygroundSupport which would signal to the playground logger that it should log the wrapped type but without considering a `CustomPlaygroundConvertible` conformance.

I also don’t think that `Optional` would get a conditional conformance to this. I’m not proposing that any standard library or corelibs types gain conformances to this protocol. Instead, it’s up to a playground logger (such as PlaygroundLogger in swift-xcode-playground-support <https://github.com/apple/swift-xcode-playground-support&gt;\) to recognize these types and handle them accordingly. The playground logger would look through the `Optional` so that this would effectively be true, but ideally the log data generated by a logger would indicate that it was wrapped by `Optional.some`.

Why not? I understand that that is how the old algorithm worked, but it contained a lot of special case hacks due to the state of Swift 1 :-). This is a chance to dissolve those away.

It’s a feature that Optional (and other standard library/corelibs/OS types) don’t conform to CustomPlaygroundRepresentable. In my mind, it’s the role of the PlaygroundLogger library to understand the types for which it wishes to generate an opaque representation instead of the standard/fallback structured representation. So Optional, String, Int, UIColor, NSView, etc. don’t themselves conform to CustomPlaygroundRepresentable — they’re not customizing their presentation in a playground.

IMO, this was the right Swift 1 attitude: "make it work at all costs" but this is not the right approach for Swift over the long term, and not the right answer for Swift 5 in particular.

Swift is still very young and immature in some ways, but it it is intentionally design for extensibility and to be used in surprising and delightful ways. It isn’t an accident of history that Int and Bool are defined in the standard library instead of being burned into the compiler.

IMO, every time you choose to privilege “well known” types in the standard library with special code in Xcode (or some other high level system loosely integrated with swift.org <http://swift.org/&gt;\) you are introducing technical debt into the Swift ecosystem. This is because you are violating the basic principles on which Swift was established, which is that users can [re]define primitives to build their own abstractions and carve out new domains beyond what was originally envisioned when you’re writing the UI code.

I am trying to figure out a way to restructure this without PlaygroundLogger knowing about the core types but am at a loss. PlaygroundLogger is fundamentally this function:

  func log(instance: Any) -> Data

where the returned `Data` contains a well-formed log packet containing either a structured or opaque log entry. Structured log entries come “for free” thanks to Mirror, so the opaque log entry case is the only interesting one. Opaque log entries have documented <https://github.com/apple/swift-xcode-playground-support/blob/4a73bf895b6fd9e5f72aad441869ab597e9e3fc3/PlaygroundLogger/Documentation/LoggerFormat.md&gt; format for the kinds of data which they could contain, so conceivably an API could be exposed that would allow a type to return a `Data` matching that format. That wouldn’t be great, though, as it would mean that the format would become API instead of something negotiated between PlaygroundLogger and its clients. (Since that’s a reasonable internal implementation strategy, though, that’s how I model things in the new PlaygroundLogger implementation.)

So if PlaygroundLogger isn’t going to accept raw `Data` values as input, it needs to take *something*. I don’t think it makes a ton of sense for PlaygroundLogger/PlaygroundSupport to define a bunch of intermediate types, nor do I think it is PlaygroundLogger/PlaygroundSupport’s role to define protocols for what it means to be an image, color, view, integer, etc. (Perhaps that’s the sticking point here?) And even if it did provide those protocols, there’d be an issue surrounding how PlaygroundLogger would choose an implementation if a type implemented multiple of these core protocols. (I discuss that briefly as a rejected alternative, as my first thinking for this problem was that I should introduce a suite of protocols like `CustomNSViewConvertible`, `CustomIntConvertible`, `CustomUIColorConvertible`, and so on.)

Previously, the standard library and PlaygroundLogger solved this with the PlaygroundQuickLook enum. For the reasons listed in my proposal, I think that the PlaygroundQuickLook enum is harmful, so I wouldn’t want to solve this problem that way.

So given that, I think it’s not unreasonable for a platform/toolchain’s playground logger to know about that platform/toolchain’s important types so it can produce the appropriate opaque log entries. If there’s some approach that I’m overlooking — or if I’m dismissing the suite of protocols case too quickly — I would love to hear it, because I’m open to suggestions.

Semi-related to this proposal, I’m working on a rewrite of the PlaygroundLogger library (currently at <https://github.com/cwakamo/swift-xcode-playground-support/tree/runtime-framework-and-improved-logger&gt;\) which makes it so that the only special standard library behavior it depends on is Mirror — it no longer relies on the Swift runtime (via PlaygroundQuickLook(reflecting:)) to figure out what to log, instead opting to check protocol conformances itself. So if this proposal is accepted into Swift, concurrent with that we’ll have a new PlaygroundLogger implementation which gets rid of as many hacks as possible.

****awesome****

I hope that someday when we have a better API than Mirror (e.g. that supports mutation) that allows reflecting on stored properties, methods, and all of the other things that Swift needs to eventually support that you’ll switch to it. I understand that the glorious future isn’t very helpful to you today though.

Yes, I very much look forward to being able to migrate off of Mirror when that time comes.

`CustomPlaygroundLoggable` would be a little clunkier to implement than `CustomPlaygroundRepresentable` is, as in the common case folks would have to write `return .custom(…)`. It’s possible that the clarity and additional flexibility this grants outweighs that cost; I’m not sure, and would love feedback on that.

I just don’t understand the usecase for “conditional customizing” at all. By way of example, we don’t have the ability to do that with CustomStringConvertible. What is different about this case?

I think the big difference with CustomStringConvertible is that it’s possible for a conformance to reimplement the default behavior on its own. For instance, if I have:

  enum Foo {
    case one(Any)
    case two
  }

As noted above, recovering the default behavior with CustomPlaygroundRepresentable is not always possible if the return type is `Any`. That very well might be an acceptable trade-off to keep the API simple.

Why can’t an implementation just return “.two” instead of nil?

Is the problem the “one” case? If that is the problem, then it might be better to take a completely different approach where you embrace the fact that you have structured data producing 2D results that need to be displayed. Each result could either return an atomic result or a result that wraps some other recursive 2D presentation.

No, the issue is that the playground logger would see that `Foo.two` conforms to `CustomPlaygroundConvertible`, and would therefore call `playgroundDescription` another time, which would return another `Foo.two`, and this would continue endlessly. I plan to guard against that in PlaygroundLogger with a limit on chaining, but this protocol should not require a failsafe as a feature.

This chaining is so that `Foo` can return a value of `Bar` which itself conforms to `CustomPlaygroundConvertible` — the promise of the API is that a value will be logged as if it were the value returned from `playgroundDescription`, so that the following lines:

  let foo = Foo()
  let bar = Bar()

produce the same log output (modulo things like type name).

Fundamentally though, playground presentation is solving several different problems:

1. Types with no special behavior can always be represented as strings, which is handled by base protoocols.
2. Some types want custom string representations to show up in playgrounds, but not in “print” and string interpolation.
3. Some types want to provide a 2D “quicklook” style presentation in addition and beyond the string representation.
4. Fewer types want to provide an animated 2d representation, that is either “live” or precomputed.

I’d suggest that this protocol only tackle problems 2 and 3, but you should cleanly distinguish between them, probably with a bespoke enum or struct that models these capabilities.

This is not quite accurate. All types have both a textual presentation and a “quicklook”-style presentation. I’d characterize this as:

  1. PlaygroundLogger determines if the subject being logged conforms to `CustomPlaygroundConvertible`. If so, it calls `playgroundDescription` and starts over, treating the return value as its subject.
  2. PlaygroundLogger then produces the data necessary to construct a “quicklook”-style presentation by doing the following:
    a) If the subject is a type known to PlaygroundLogger (modeled as a conformance to a PlaygroundLogger-internal protocol), then PlaygroundLogger produces opaque log data for that type’s “quicklook”-style presentation. (As examples: for String, it encodes UTF-8 bytes; for CGPoint, it encodes the x and y points using an NSKeyedArchiver; for UIView, it encodes a rendered image; etc.)
    b) Otherwise, PlaygroundLogger constructs a Mirror for the subject and produces structured log data representing the subject for a structural “quicklook”-style presentation.
  3. PlaygroundLogger then uses `String(describing:)` on the subject being logged to produce the textual representation. (In some cases, IDEs may ignore this value in favor of textual-ish information about the “quicklook”-style presentation, such as color swatch or the dimensions of the logged image.)

PlaygroundLogger does not support producing animated representations.

The `CustomPlaygroundConvertible` protocol is all-or-nothing; if you implement it, both your textual and “quicklook”-style presentations are modified to match whatever you returned. There isn’t a facility to only affect one presentation or the other. This provides parity with `CustomPlaygroundQuickLookable` but with a more flexible interface, which is the intent of this proposal.

Connor

···

On Jan 11, 2018, at 11:51 PM, Chris Lattner <clattner@nondot.org> wrote:
On Jan 11, 2018, at 11:22 AM, Connor Wakamo <cwakamo@apple.com <mailto:cwakamo@apple.com>> wrote:

I do like the `playgroundDescription` name for the property, but am a little hesitant to use the name `CustomPlaygroundConvertible` because conforming types can’t be converted to playgrounds. I can’t come up with an appropriate word in `CustomPlaygroundThingConvertible` to use in place of `Thing`, though. (If we end up pivoting to the enum I described above then something like `CustomPlaygroundLoggable` would be more appropriate.)

I would strongly recommend aligning with the state of the art in CustomStringConvertible (which has been extensively discussed) and ignore the precedent in the existing playground logging stuff (which hasn’t).

That’s very reasonable. I’ll update the proposal to use CustomPlaygroundConvertible (unless I or someone else can come up with a really good “Thing” for a name like CustomPlaygroundThingConvertible, as that would even better match CustomStringConvertible/CustomDebugStringConvertible).

Thank you!

-Chris

I'm a heavy user of `CustomPlaygroundQuickLookable` (for example, https://i.imgur.com/zOa5OUZ.jpg\). Although I'm happy with the current name, to make it more in line with other convertibles, perhaps:

* CustomPlaygroundQuickLookConvertible
* CustomPlaygroundDisplayConvertible
* CustomPlaygroundValuePresentationConvertible
* CustomPlaygroundPresentationConvertible

-- E

···

On Jan 11, 2018, at 1:40 PM, Jonathan Hull via swift-evolution <swift-evolution@swift.org> wrote:

On Jan 11, 2018, at 11:22 AM, Connor Wakamo via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

That’s very reasonable. I’ll update the proposal to use CustomPlaygroundConvertible (unless I or someone else can come up with a really good “Thing” for a name like CustomPlaygroundThingConvertible, as that would even better match CustomStringConvertible/CustomDebugStringConvertible).

CustomPlaygroundPreviewConvertible?

Saagar Jha

Well, in my experience performance issues tend to come not from trying to display a single object, but when you have an aggregation of many objects. For example, displaying one Int is pretty lightweight, as is an [Int] with a handful of elements, but displaying an [Int] with million elements is not (this is assuming that collections will run the playgroundRepresentation on their individual elements as they do currently). I don’t think the current proposal can solve this issue as it’s pitched currently. (For those curious, this doesn’t have a good solution today, either: the best “solution” I’ve been able to come up with is to move performance-sensitive out into a Playground’s Sources folder, which isn’t displayed.)

Yes, this proposal does not affect this case: the playground transform will still instrument all of the source code in the main source file. This proposal is merely about deprecating/removing a substandard API for controlling how values are presented in playgrounds with a better, more flexible one. (Though see my other reply to Chris where I present an alternative which could be extended to include support for disabling logging altogether for an instance.)

Fundamentally, I don’t think it’s clear what the “default” value is supposed to be: we already have three different interpretations of what it could do: do literally nothing (i.e. display an empty representation), use a superclass’s representation, or use a structural representation based on whether the type is an enum, struct, class, etc. I think we need to clear up what the default actually means (and if we even need it at all) before we can proceed.

This API is a bit wishy-washy as to what the “default” is. That’s somewhat intentional — the default presentation of an arbitrary Swift value/object is defined by the IDE, not by an API in Swift. I think my terminology is a bit confused here, and I’ll try to address that in a revision of the proposal. Let me try to clarify this a bit:

The fundamental design of playgrounds is that the compiler will insert calls to a logging function which is effectively required to take an `Any`. So therefore every instance of every type must be loggable by the playground logger. The PlaygroundLogger framework in swift-xcode-playground-support <https://github.com/apple/swift-xcode-playground-support&gt; implements this by generating either structured log entries using `Mirror` or specialized, opaque (aka IDERepr) log entries. PlaygroundLogger will generate a structured log entry for most instances, but for instances of special types it knows about (e.g. String, Int, NSColor, UIView, etc.) it will generate an opaque log entry which an IDE can then consume and display as is appropriate.

The CustomPlaygroundRepresentable API I’ve proposed does exactly one thing: it allows instances of conforming types to provide an optional stand-in to be used by the playground logger. So PlaygroundLogger would handle the following cases thusly:

  - If a type conforms to CustomPlaygroundRepresentable and returns a non-nil value, PlaygroundLogger will generate the appropriate log entry for the returned value
  - If a type conforms to CustomPlaygroundRepresentable and returns nil, PlaygroundLogger will generate the appropriate log entry for `self`
    - If `self` is one of the types (or a subclass of one of the types) for which PlaygroundLogger generates an opaque entry, PlaygroundLogger will generate an opaque log entry
    - Otherwise, PlaygroundLogger will generate a structured log entry

Just wanted to make it totally clear here: is the superclass selected the one that is the closest to the current type, or the one that is the most distant parent? Referring back to the example I had further upthread:

class FooView: UIView {
  override var playgroundRepresentation: Any? {
    return “foo”
  }
}

class BarView: FooView {
  override var playgroundRepresentation: Any? {
    return nil
  }
}

Does BarView show up as a UIView, or a FooView? If it’s shown as a FooView, then returning nil is superfluous: you can just not override playgroundRepresentation and it’ll pick up the one from FooView.

Ah, so here’s what it would do: since BarView conforms to CustomPlaygroundRepresentable, the playground logger would get the playgroundRepresentation. Since playgroundRepresentation returned nil, the playground logger will treat that instance of BarView as if it did not conform to CustomPlaygroundRepresentable, and then log it according to the playground logger’s normal rules. Since the PlaygroundLogger library is capable of logging a picture of a UIView, it will generate a log entry containing a picture of that instance of BarView. (My apologies if the distinction between “the playground logger” and “the PlaygroundLogger library” is subtle — the API is not tied to a particular logger implementation, so while the PlaygroundLogger library in swift-xcode-playground-support supports generating an opaque log entry for UIViews, it’s theoretically possible that a different logger implementation would not be able to and would treat UIViews as a regular class.)

If BarView always wants to just use the superclass’s implementation of CustomPlaygroundRepresentable, then it should just not override the property. If it wants to occasionally use it, then it should occasionally return `super.playgroundRepresentation`. (If BarView always wants to be logged as itself — that is, like a UIView — then overriding playgroundRepresentation to always return nil would be appropriate.)

One important thing to note here: UIView does not conform to CustomPlaygroundRepresentable. UIView does not itself wish to be logged as some other object/value; it wants to be logged as a UIView. So the example code provided would actually be:

  class FooView: UIView, CustomPlaygroundRepresentable {
    var playgroundRepresentation: Any? {
      return "foo"
    }
  }

  class BarView: FooView {
    override var playgroundRepresentation: Any? {
      return nil
    }
  }

Connor

···

On Jan 10, 2018, at 2:34 PM, Saagar Jha <saagar@saagarjha.com> wrote:

On Jan 10, 2018, at 14:10, Connor Wakamo <cwakamo@apple.com <mailto:cwakamo@apple.com>> wrote:

On Jan 10, 2018, at 12:39 PM, Saagar Jha <saagar@saagarjha.com <mailto:saagar@saagarjha.com>> wrote:

This process if potentially recursive (likely up to an implementation-defined limit): if `Foo: CustomPlaygroundRepresentable` and `Bar: CustomPlaygroundRepresentable`, and a `Foo` instance returns an instance of `Bar` from `playgroundRepresentation`, then the playground logger should effectively log `self.playgroundRepresentation.playgroundRepresentation`.

Connor

The second case, and the one I’m more worried about, is subclassing. If, for instance, you have the following:

  class FooView: UIView, CustomPlaygroundRepresentable {
    var playgroundRepresentation: Any {
      return “A string describing this view"
    }
  }

  class BarView: FooView {
    override var playgroundRepresentation: Any {
      // BarView instances wanted to be logged as themselves, but there’s no way to express that
      return ???
    }
  }

There’s nothing that BarView can do to ensure it gets logged like a view because FooView declared a conformance to CustomPlaygroundRepresentable.

I really don’t understand this. FooView declares that it conforms, and it provides a “playgroundRepresentation” member. Cool for FooView.

BarView comes around and overrides FooView’s implementation. They don’t have to conform, because it *isa* FooView, so of course it conforms. If it wants to customize its presentation, it overrides its “playgroundRepresentation” method and it… just works. If it conditionally wants the FooView representation for some reason, it can even call “super.playgroundRepresentation”. What’s the problem? This seems ideal to me.

The issue is that there’s no way for `BarView` to recover the default playground representation which `UIView` and its subclasses get.

I get it now, thanks.

For a `UIView`, the PlaygroundLogger library renders an image of the view and packages that up in a log entry. If `BarView` wants that instead of `FooView`’s custom representation, there’s no way for it to request it.

I can think of two different ways to solve this problem:

1) Go with your suggestion upthread, where a custom enum is used: implementations do:

  if predicate() {
    return .custom(“my representation)
  } else {
    return .default
  }

The problem with this is that you’re forcing implementations of this to deal with the enum just because of the rare case. Not a showstopper, but doesn’t feel right.

2) Go with the simple approach of returning Any, and add a new function to the Playground API somewhere that produces the default representation that they can call, the unusual case above would look like this:

  if predicate() {
    return “my representation
  } else {
    return SomePlaygroundAPI.getDefaultPlaygroundRepresentation(self)
  }

This seems like the best of both worlds to me.

IMO, every time you choose to privilege “well known” types in the standard library with special code in Xcode (or some other high level system loosely integrated with swift.org <http://swift.org/&gt;\) you are introducing technical debt into the Swift ecosystem. This is because you are violating the basic principles on which Swift was established, which is that users can [re]define primitives to build their own abstractions and carve out new domains beyond what was originally envisioned when you’re writing the UI code.

So given that, I think it’s not unreasonable for a platform/toolchain’s playground logger to know about that platform/toolchain’s important types so it can produce the appropriate opaque log entries. If there’s some approach that I’m overlooking — or if I’m dismissing the suite of protocols case too quickly — I would love to hear it, because I’m open to suggestions.

Yes, you are completely right: Unless you are willing to make the structured schema public, there is no point to doing what I suggest.

I do think that there is a schema which would make sense to take public (similar to the abstract format used in treeviews like the navigator and UI debugger for variables, strikingly similar to what JSON can express with a fancier terminal than strings), but there is no rush to do that, and it is best to take time to consider it deliberately.

Is the problem the “one” case? If that is the problem, then it might be better to take a completely different approach where you embrace the fact that you have structured data producing 2D results that need to be displayed. Each result could either return an atomic result or a result that wraps some other recursive 2D presentation.

No, the issue is that the playground logger would see that `Foo.two` conforms to `CustomPlaygroundConvertible`, and would therefore call `playgroundDescription` another time, which would return another `Foo.two`, and this would continue endlessly. I plan to guard against that in PlaygroundLogger with a limit on chaining, but this protocol should not require a failsafe as a feature.

Right, it seems like the best way to solve this is to provide explicit access to the default presentation.

-Chris

···

On Jan 12, 2018, at 6:22 PM, Connor Wakamo <cwakamo@apple.com> wrote:

I don’t think we can change this to return `Any` instead of `Any?`. I think there are potentially cases where a developer might want to selectively opt-in to this behavior.

Which cases? How important are they?

I can think of a couple of cases where this could be useful.

The first is an enum. Extending Riley’s example from earlier in the thread:

  enum MyUnion {
    case string(String)
    case image(UIImage)
    case intPair(Int, Int)
    case none
  }

This enum might want to present the string and image cases as strings and images, but treat the intPair and none cases the “default” way for the enum. This is probably not the most compelling example as there is a workaround — return a second enum or other structured type from playgroundRepresentation — but that feels not great.

IMO, this is sugaring something that doesn’t need to be sugared. There are simple solutions to this problem without complicating the design of your feature. The few people who care about this can write it out long hand.

Agreed.

The second case, and the one I’m more worried about, is subclassing. If, for instance, you have the following:

  class FooView: UIView, CustomPlaygroundRepresentable {
    var playgroundRepresentation: Any {
      return “A string describing this view"
    }
  }

  class BarView: FooView {
    override var playgroundRepresentation: Any {
      // BarView instances wanted to be logged as themselves, but there’s no way to express that
      return ???
    }
  }

There’s nothing that BarView can do to ensure it gets logged like a view because FooView declared a conformance to CustomPlaygroundRepresentable.

I really don’t understand this. FooView declares that it conforms, and it provides a “playgroundRepresentation” member. Cool for FooView.

BarView comes around and overrides FooView’s implementation. They don’t have to conform, because it *isa* FooView, so of course it conforms. If it wants to customize its presentation, it overrides its “playgroundRepresentation” method and it… just works. If it conditionally wants the FooView representation for some reason, it can even call “super.playgroundRepresentation”. What’s the problem? This seems ideal to me.

The issue is that there’s no way for `BarView` to recover the default playground representation which `UIView` and its subclasses get. For a `UIView`, the PlaygroundLogger library renders an image of the view and packages that up in a log entry. If `BarView` wants that instead of `FooView`’s custom representation, there’s no way for it to request it.

That being said, I think I’m convinced that this is enough of an edge case that shouldn’t complicate the design of the protocol. So in my updated proposal I’ll be going with:

  protocol CustomPlaygroundConvertible {
    var playgroundDescription: Any { get }
  }

If we find out that this isn’t as much of an edge case as thought, we can add a type like `DefaultPlaygroundRepresentation<Wrapped>` to PlaygroundSupport which would signal to the playground logger that it should log the wrapped type but without considering a `CustomPlaygroundConvertible` conformance.

(Replying to future comments as well here): so the hierarchy looks like: NSObject -> UIView -> FooView -> BarView, and we’re saying that BarView should be able to override FooView’s override and drop down one of its ancestors implementations?

IMO, the only way to do this is to return “super.super.playgroundDescription” (if BarView wants to present as a UIView) or “super.super.super.playgroundDescription” (if it wants to present as an NSObject — imagine NSObject had some custom representation).

If it just returned “.default”, how would the logger know which superclass implementation to use (UIView or NSObject)?

I also don’t think that `Optional` would get a conditional conformance to this. I’m not proposing that any standard library or corelibs types gain conformances to this protocol. Instead, it’s up to a playground logger (such as PlaygroundLogger in swift-xcode-playground-support <https://github.com/apple/swift-xcode-playground-support&gt;\) to recognize these types and handle them accordingly. The playground logger would look through the `Optional` so that this would effectively be true, but ideally the log data generated by a logger would indicate that it was wrapped by `Optional.some`.

Why not? I understand that that is how the old algorithm worked, but it contained a lot of special case hacks due to the state of Swift 1 :-). This is a chance to dissolve those away.

It’s a feature that Optional (and other standard library/corelibs/OS types) don’t conform to CustomPlaygroundRepresentable. In my mind, it’s the role of the PlaygroundLogger library to understand the types for which it wishes to generate an opaque representation instead of the standard/fallback structured representation. So Optional, String, Int, UIColor, NSView, etc. don’t themselves conform to CustomPlaygroundRepresentable — they’re not customizing their presentation in a playground.

IMO, this was the right Swift 1 attitude: "make it work at all costs" but this is not the right approach for Swift over the long term, and not the right answer for Swift 5 in particular.

Swift is still very young and immature in some ways, but it it is intentionally design for extensibility and to be used in surprising and delightful ways. It isn’t an accident of history that Int and Bool are defined in the standard library instead of being burned into the compiler.

IMO, every time you choose to privilege “well known” types in the standard library with special code in Xcode (or some other high level system loosely integrated with swift.org <http://swift.org/&gt;\) you are introducing technical debt into the Swift ecosystem. This is because you are violating the basic principles on which Swift was established, which is that users can [re]define primitives to build their own abstractions and carve out new domains beyond what was originally envisioned when you’re writing the UI code.

I am trying to figure out a way to restructure this without PlaygroundLogger knowing about the core types but am at a loss. PlaygroundLogger is fundamentally this function:

  func log(instance: Any) -> Data

where the returned `Data` contains a well-formed log packet containing either a structured or opaque log entry. Structured log entries come “for free” thanks to Mirror, so the opaque log entry case is the only interesting one. Opaque log entries have documented <https://github.com/apple/swift-xcode-playground-support/blob/4a73bf895b6fd9e5f72aad441869ab597e9e3fc3/PlaygroundLogger/Documentation/LoggerFormat.md&gt; format for the kinds of data which they could contain, so conceivably an API could be exposed that would allow a type to return a `Data` matching that format. That wouldn’t be great, though, as it would mean that the format would become API instead of something negotiated between PlaygroundLogger and its clients. (Since that’s a reasonable internal implementation strategy, though, that’s how I model things in the new PlaygroundLogger implementation.)

So if PlaygroundLogger isn’t going to accept raw `Data` values as input, it needs to take *something*. I don’t think it makes a ton of sense for PlaygroundLogger/PlaygroundSupport to define a bunch of intermediate types, nor do I think it is PlaygroundLogger/PlaygroundSupport’s role to define protocols for what it means to be an image, color, view, integer, etc. (Perhaps that’s the sticking point here?) And even if it did provide those protocols, there’d be an issue surrounding how PlaygroundLogger would choose an implementation if a type implemented multiple of these core protocols. (I discuss that briefly as a rejected alternative, as my first thinking for this problem was that I should introduce a suite of protocols like `CustomNSViewConvertible`, `CustomIntConvertible`, `CustomUIColorConvertible`, and so on.)

I don’t think it needs to define protocols for what it means to be an image, but it could define a general “playground log formatter” abstraction which can translate an object in to a log entry (with a default implementation based on Mirror).

That could be a way to solve the problem above. The core types could simply expose formatter objects, and CustomPlaygroundRepresentable could just ask each instance explicitly for the formatter it wants to use (falling-back to the Mirror-based one if the custom formatter fails or the object asks explicitly for no special formatting).

Previously, the standard library and PlaygroundLogger solved this with the PlaygroundQuickLook enum. For the reasons listed in my proposal, I think that the PlaygroundQuickLook enum is harmful, so I wouldn’t want to solve this problem that way.

So given that, I think it’s not unreasonable for a platform/toolchain’s playground logger to know about that platform/toolchain’s important types so it can produce the appropriate opaque log entries. If there’s some approach that I’m overlooking — or if I’m dismissing the suite of protocols case too quickly — I would love to hear it, because I’m open to suggestions.

Lets say we have something like a formatter abstraction:

/* sealed */ protocol PlaygroundLogEntry { }

// We can expose concrete implementations of PlaygroundLogEntry as we like. We might also have private entry formats.
// It might also be an enum with private cases, when we get that feature.

protocol PlaygroundFomatter {
    func format(object: Any) -> PlaygroundLogEntry? // may fail if object type is unexpected.
}

protocol CustomPlaygroundRepresentable {
    var playgroundFormatter: PlaygroundFormatter? { get } // if nil, uses the minimal, Mirror-based formatter.
}

PlaygroundLogger/Support can build in some basic formatters for stuff like some PNG-formatted bytes, RGB colours, OpenGL textures. Stuff which is reasonably useful across platforms.

struct PNGImageFormatter: PlaygroundFormatter {
    func format(object: Any) -> PlaygroundLogEntry? {
        guard let imageData = object as? Data else { return nil } // unexpected type.
        // Parse the image data and return something the playground can understand.
    }
}

But it might also provide some optimised formatters for OS types, like UIView.

struct UIViewFormatter: PlaygroundFormatter {
    func format(object: Any) -> PlaygroundLogEntry? { /* magic */ }
}

extension UIView: CustomPlaygroundRepresentable {
    static var defaultPlaygroundFormatter: PlaygroundFormatter { return UIViewFormatter() }

    var playgroundFormatter: PlaygroundFormatter? { return UIView.defaultPlaygroundFormatter }
}

Also, one more handy formatter:

struct KeyPathFormatter<T>: PlaygroundFormatter {
    let base: PlaygroundFormatter
    let dataKP: KeyPath<T, PlaygroundFormatter>

    func format(object: Any) -> PlaygroundLogEntry? {
        return base.format(object: object[keyPath: dataKP])
    }
}

Then, when you’re porting the rich graphical previews to a new platform, you can map your base-level UI objects in terms the formatter knows about, or write your own for any public log-entry formats (if there are any).

class OpenGLView {
    var formatter: PlaygroundFormatter? {
        return KeyPathFormatter(base: PlaygroundSupport.OpenGLTextureFormatter(format: self.textureFormat), keyPath: \.backingTexture)
    }
}

This way, sure, PlaygroundLogger/Support has some built-in behaviours for OS types it knows about. That said, I suppose it would be open for anybody to add their own if it’s useful for the community. We don’t have a way in Swift yet to make sealed protocols, but an enum with private cases might allow us to model this well. Nobody is going to switch over it, so it should be fine :P

···

On 13. Jan 2018, at 03:22, Connor Wakamo via swift-evolution <swift-evolution@swift.org> wrote:

On Jan 11, 2018, at 11:51 PM, Chris Lattner <clattner@nondot.org <mailto:clattner@nondot.org>> wrote:
On Jan 11, 2018, at 11:22 AM, Connor Wakamo <cwakamo@apple.com <mailto:cwakamo@apple.com>> wrote:

Semi-related to this proposal, I’m working on a rewrite of the PlaygroundLogger library (currently at <https://github.com/cwakamo/swift-xcode-playground-support/tree/runtime-framework-and-improved-logger&gt;\) which makes it so that the only special standard library behavior it depends on is Mirror — it no longer relies on the Swift runtime (via PlaygroundQuickLook(reflecting:)) to figure out what to log, instead opting to check protocol conformances itself. So if this proposal is accepted into Swift, concurrent with that we’ll have a new PlaygroundLogger implementation which gets rid of as many hacks as possible.

****awesome****

I hope that someday when we have a better API than Mirror (e.g. that supports mutation) that allows reflecting on stored properties, methods, and all of the other things that Swift needs to eventually support that you’ll switch to it. I understand that the glorious future isn’t very helpful to you today though.

Yes, I very much look forward to being able to migrate off of Mirror when that time comes.

`CustomPlaygroundLoggable` would be a little clunkier to implement than `CustomPlaygroundRepresentable` is, as in the common case folks would have to write `return .custom(…)`. It’s possible that the clarity and additional flexibility this grants outweighs that cost; I’m not sure, and would love feedback on that.

I just don’t understand the usecase for “conditional customizing” at all. By way of example, we don’t have the ability to do that with CustomStringConvertible. What is different about this case?

I think the big difference with CustomStringConvertible is that it’s possible for a conformance to reimplement the default behavior on its own. For instance, if I have:

  enum Foo {
    case one(Any)
    case two
  }

As noted above, recovering the default behavior with CustomPlaygroundRepresentable is not always possible if the return type is `Any`. That very well might be an acceptable trade-off to keep the API simple.

Why can’t an implementation just return “.two” instead of nil?

Is the problem the “one” case? If that is the problem, then it might be better to take a completely different approach where you embrace the fact that you have structured data producing 2D results that need to be displayed. Each result could either return an atomic result or a result that wraps some other recursive 2D presentation.

No, the issue is that the playground logger would see that `Foo.two` conforms to `CustomPlaygroundConvertible`, and would therefore call `playgroundDescription` another time, which would return another `Foo.two`, and this would continue endlessly. I plan to guard against that in PlaygroundLogger with a limit on chaining, but this protocol should not require a failsafe as a feature.

This chaining is so that `Foo` can return a value of `Bar` which itself conforms to `CustomPlaygroundConvertible` — the promise of the API is that a value will be logged as if it were the value returned from `playgroundDescription`, so that the following lines:

  let foo = Foo()
  let bar = Bar()

produce the same log output (modulo things like type name).

Fundamentally though, playground presentation is solving several different problems:

1. Types with no special behavior can always be represented as strings, which is handled by base protoocols.
2. Some types want custom string representations to show up in playgrounds, but not in “print” and string interpolation.
3. Some types want to provide a 2D “quicklook” style presentation in addition and beyond the string representation.
4. Fewer types want to provide an animated 2d representation, that is either “live” or precomputed.

I’d suggest that this protocol only tackle problems 2 and 3, but you should cleanly distinguish between them, probably with a bespoke enum or struct that models these capabilities.

This is not quite accurate. All types have both a textual presentation and a “quicklook”-style presentation. I’d characterize this as:

  1. PlaygroundLogger determines if the subject being logged conforms to `CustomPlaygroundConvertible`. If so, it calls `playgroundDescription` and starts over, treating the return value as its subject.
  2. PlaygroundLogger then produces the data necessary to construct a “quicklook”-style presentation by doing the following:
    a) If the subject is a type known to PlaygroundLogger (modeled as a conformance to a PlaygroundLogger-internal protocol), then PlaygroundLogger produces opaque log data for that type’s “quicklook”-style presentation. (As examples: for String, it encodes UTF-8 bytes; for CGPoint, it encodes the x and y points using an NSKeyedArchiver; for UIView, it encodes a rendered image; etc.)
    b) Otherwise, PlaygroundLogger constructs a Mirror for the subject and produces structured log data representing the subject for a structural “quicklook”-style presentation.
  3. PlaygroundLogger then uses `String(describing:)` on the subject being logged to produce the textual representation. (In some cases, IDEs may ignore this value in favor of textual-ish information about the “quicklook”-style presentation, such as color swatch or the dimensions of the logged image.)

PlaygroundLogger does not support producing animated representations.

The `CustomPlaygroundConvertible` protocol is all-or-nothing; if you implement it, both your textual and “quicklook”-style presentations are modified to match whatever you returned. There isn’t a facility to only affect one presentation or the other. This provides parity with `CustomPlaygroundQuickLookable` but with a more flexible interface, which is the intent of this proposal.

Connor

I do like the `playgroundDescription` name for the property, but am a little hesitant to use the name `CustomPlaygroundConvertible` because conforming types can’t be converted to playgrounds. I can’t come up with an appropriate word in `CustomPlaygroundThingConvertible` to use in place of `Thing`, though. (If we end up pivoting to the enum I described above then something like `CustomPlaygroundLoggable` would be more appropriate.)

I would strongly recommend aligning with the state of the art in CustomStringConvertible (which has been extensively discussed) and ignore the precedent in the existing playground logging stuff (which hasn’t).

That’s very reasonable. I’ll update the proposal to use CustomPlaygroundConvertible (unless I or someone else can come up with a really good “Thing” for a name like CustomPlaygroundThingConvertible, as that would even better match CustomStringConvertible/CustomDebugStringConvertible).

Thank you!

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Using the existing name for the new protocol isn’t on the table, as that would present a source compatibility issue which we couldn’t paper over. These names are interesting; I’ll at the very least include them in “alternatives considered” in my updated proposal.

Connor

···

On Jan 13, 2018, at 2:18 PM, Erica Sadun <erica@ericasadun.com> wrote:

On Jan 11, 2018, at 1:40 PM, Jonathan Hull via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Jan 11, 2018, at 11:22 AM, Connor Wakamo via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

That’s very reasonable. I’ll update the proposal to use CustomPlaygroundConvertible (unless I or someone else can come up with a really good “Thing” for a name like CustomPlaygroundThingConvertible, as that would even better match CustomStringConvertible/CustomDebugStringConvertible).

CustomPlaygroundPreviewConvertible?

I'm a heavy user of `CustomPlaygroundQuickLookable` (for example, https://i.imgur.com/zOa5OUZ.jpg\). Although I'm happy with the current name, to make it more in line with other convertibles, perhaps:

* CustomPlaygroundQuickLookConvertible
* CustomPlaygroundDisplayConvertible
* CustomPlaygroundValuePresentationConvertible
* CustomPlaygroundPresentationConvertible

The second case, and the one I’m more worried about, is subclassing. If, for instance, you have the following:

  class FooView: UIView, CustomPlaygroundRepresentable {
    var playgroundRepresentation: Any {
      return “A string describing this view"
    }
  }

  class BarView: FooView {
    override var playgroundRepresentation: Any {
      // BarView instances wanted to be logged as themselves, but there’s no way to express that
      return ???
    }
  }

There’s nothing that BarView can do to ensure it gets logged like a view because FooView declared a conformance to CustomPlaygroundRepresentable.

I really don’t understand this. FooView declares that it conforms, and it provides a “playgroundRepresentation” member. Cool for FooView.

BarView comes around and overrides FooView’s implementation. They don’t have to conform, because it *isa* FooView, so of course it conforms. If it wants to customize its presentation, it overrides its “playgroundRepresentation” method and it… just works. If it conditionally wants the FooView representation for some reason, it can even call “super.playgroundRepresentation”. What’s the problem? This seems ideal to me.

The issue is that there’s no way for `BarView` to recover the default playground representation which `UIView` and its subclasses get.

I get it now, thanks.

For a `UIView`, the PlaygroundLogger library renders an image of the view and packages that up in a log entry. If `BarView` wants that instead of `FooView`’s custom representation, there’s no way for it to request it.

I can think of two different ways to solve this problem:

1) Go with your suggestion upthread, where a custom enum is used: implementations do:

  if predicate() {
    return .custom(“my representation)
  } else {
    return .default
  }

The problem with this is that you’re forcing implementations of this to deal with the enum just because of the rare case. Not a showstopper, but doesn’t feel right.

2) Go with the simple approach of returning Any, and add a new function to the Playground API somewhere that produces the default representation that they can call, the unusual case above would look like this:

  if predicate() {
    return “my representation
  } else {
    return SomePlaygroundAPI.getDefaultPlaygroundRepresentation(self)
  }

This seems like the best of both worlds to me.

Yes, agreed. I’m not going to propose adding this to PlaygroundSupport as part of this proposal (under the assumption that this is an extreme edge case to begin with), but I’ll include a reference to it in the “alternatives considered” section so it can be referenced in the future should this be important enough that it warrants support.

IMO, every time you choose to privilege “well known” types in the standard library with special code in Xcode (or some other high level system loosely integrated with swift.org <http://swift.org/&gt;\) you are introducing technical debt into the Swift ecosystem. This is because you are violating the basic principles on which Swift was established, which is that users can [re]define primitives to build their own abstractions and carve out new domains beyond what was originally envisioned when you’re writing the UI code.

So given that, I think it’s not unreasonable for a platform/toolchain’s playground logger to know about that platform/toolchain’s important types so it can produce the appropriate opaque log entries. If there’s some approach that I’m overlooking — or if I’m dismissing the suite of protocols case too quickly — I would love to hear it, because I’m open to suggestions.

Yes, you are completely right: Unless you are willing to make the structured schema public, there is no point to doing what I suggest.

I do think that there is a schema which would make sense to take public (similar to the abstract format used in treeviews like the navigator and UI debugger for variables, strikingly similar to what JSON can express with a fancier terminal than strings), but there is no rush to do that, and it is best to take time to consider it deliberately.

Likewise, agreed — I don’t want to try to figure out what that would look like with the time pressure imposed by trying to get this in Swift 4.1 prior to ABI stability. And even in a world where this is exposed in a way to allow custom implementations at that low level, I think that a `CustomPlaygroundConvertible`-style API is still useful as the simple way of customizing display for most clients.

Connor

···

On Jan 12, 2018, at 11:02 PM, Chris Lattner <clattner@nondot.org> wrote:

On Jan 12, 2018, at 6:22 PM, Connor Wakamo <cwakamo@apple.com <mailto:cwakamo@apple.com>> wrote:

Is the problem the “one” case? If that is the problem, then it might be better to take a completely different approach where you embrace the fact that you have structured data producing 2D results that need to be displayed. Each result could either return an atomic result or a result that wraps some other recursive 2D presentation.

No, the issue is that the playground logger would see that `Foo.two` conforms to `CustomPlaygroundConvertible`, and would therefore call `playgroundDescription` another time, which would return another `Foo.two`, and this would continue endlessly. I plan to guard against that in PlaygroundLogger with a limit on chaining, but this protocol should not require a failsafe as a feature.

Right, it seems like the best way to solve this is to provide explicit access to the default presentation.

-Chris

I don’t think we can change this to return `Any` instead of `Any?`. I think there are potentially cases where a developer might want to selectively opt-in to this behavior.

Which cases? How important are they?

I can think of a couple of cases where this could be useful.

The first is an enum. Extending Riley’s example from earlier in the thread:

  enum MyUnion {
    case string(String)
    case image(UIImage)
    case intPair(Int, Int)
    case none
  }

This enum might want to present the string and image cases as strings and images, but treat the intPair and none cases the “default” way for the enum. This is probably not the most compelling example as there is a workaround — return a second enum or other structured type from playgroundRepresentation — but that feels not great.

IMO, this is sugaring something that doesn’t need to be sugared. There are simple solutions to this problem without complicating the design of your feature. The few people who care about this can write it out long hand.

Agreed.

The second case, and the one I’m more worried about, is subclassing. If, for instance, you have the following:

  class FooView: UIView, CustomPlaygroundRepresentable {
    var playgroundRepresentation: Any {
      return “A string describing this view"
    }
  }

  class BarView: FooView {
    override var playgroundRepresentation: Any {
      // BarView instances wanted to be logged as themselves, but there’s no way to express that
      return ???
    }
  }

There’s nothing that BarView can do to ensure it gets logged like a view because FooView declared a conformance to CustomPlaygroundRepresentable.

I really don’t understand this. FooView declares that it conforms, and it provides a “playgroundRepresentation” member. Cool for FooView.

BarView comes around and overrides FooView’s implementation. They don’t have to conform, because it *isa* FooView, so of course it conforms. If it wants to customize its presentation, it overrides its “playgroundRepresentation” method and it… just works. If it conditionally wants the FooView representation for some reason, it can even call “super.playgroundRepresentation”. What’s the problem? This seems ideal to me.

The issue is that there’s no way for `BarView` to recover the default playground representation which `UIView` and its subclasses get. For a `UIView`, the PlaygroundLogger library renders an image of the view and packages that up in a log entry. If `BarView` wants that instead of `FooView`’s custom representation, there’s no way for it to request it.

That being said, I think I’m convinced that this is enough of an edge case that shouldn’t complicate the design of the protocol. So in my updated proposal I’ll be going with:

  protocol CustomPlaygroundConvertible {
    var playgroundDescription: Any { get }
  }

If we find out that this isn’t as much of an edge case as thought, we can add a type like `DefaultPlaygroundRepresentation<Wrapped>` to PlaygroundSupport which would signal to the playground logger that it should log the wrapped type but without considering a `CustomPlaygroundConvertible` conformance.

(Replying to future comments as well here): so the hierarchy looks like: NSObject -> UIView -> FooView -> BarView, and we’re saying that BarView should be able to override FooView’s override and drop down one of its ancestors implementations?

IMO, the only way to do this is to return “super.super.playgroundDescription” (if BarView wants to present as a UIView) or “super.super.super.playgroundDescription” (if it wants to present as an NSObject — imagine NSObject had some custom representation).

If it just returned “.default”, how would the logger know which superclass implementation to use (UIView or NSObject)?

My answer to this is a bit of a deflection — this isn’t a case PlaygroundLogger currently has to worry about, as it’s “natively-supported” opaquely-represented types don’t have any common typing, aside from NSObject which isn’t opaquely-represented. But it does mean that if we introduce an API in PlaygroundSupport to request the “default” representation for an object, we may want to include a variant/overload of that which lets a developer clarify what they mean by “default”.

I also don’t think that `Optional` would get a conditional conformance to this. I’m not proposing that any standard library or corelibs types gain conformances to this protocol. Instead, it’s up to a playground logger (such as PlaygroundLogger in swift-xcode-playground-support <https://github.com/apple/swift-xcode-playground-support&gt;\) to recognize these types and handle them accordingly. The playground logger would look through the `Optional` so that this would effectively be true, but ideally the log data generated by a logger would indicate that it was wrapped by `Optional.some`.

Why not? I understand that that is how the old algorithm worked, but it contained a lot of special case hacks due to the state of Swift 1 :-). This is a chance to dissolve those away.

It’s a feature that Optional (and other standard library/corelibs/OS types) don’t conform to CustomPlaygroundRepresentable. In my mind, it’s the role of the PlaygroundLogger library to understand the types for which it wishes to generate an opaque representation instead of the standard/fallback structured representation. So Optional, String, Int, UIColor, NSView, etc. don’t themselves conform to CustomPlaygroundRepresentable — they’re not customizing their presentation in a playground.

IMO, this was the right Swift 1 attitude: "make it work at all costs" but this is not the right approach for Swift over the long term, and not the right answer for Swift 5 in particular.

Swift is still very young and immature in some ways, but it it is intentionally design for extensibility and to be used in surprising and delightful ways. It isn’t an accident of history that Int and Bool are defined in the standard library instead of being burned into the compiler.

IMO, every time you choose to privilege “well known” types in the standard library with special code in Xcode (or some other high level system loosely integrated with swift.org <http://swift.org/&gt;\) you are introducing technical debt into the Swift ecosystem. This is because you are violating the basic principles on which Swift was established, which is that users can [re]define primitives to build their own abstractions and carve out new domains beyond what was originally envisioned when you’re writing the UI code.

I am trying to figure out a way to restructure this without PlaygroundLogger knowing about the core types but am at a loss. PlaygroundLogger is fundamentally this function:

  func log(instance: Any) -> Data

where the returned `Data` contains a well-formed log packet containing either a structured or opaque log entry. Structured log entries come “for free” thanks to Mirror, so the opaque log entry case is the only interesting one. Opaque log entries have documented <https://github.com/apple/swift-xcode-playground-support/blob/4a73bf895b6fd9e5f72aad441869ab597e9e3fc3/PlaygroundLogger/Documentation/LoggerFormat.md&gt; format for the kinds of data which they could contain, so conceivably an API could be exposed that would allow a type to return a `Data` matching that format. That wouldn’t be great, though, as it would mean that the format would become API instead of something negotiated between PlaygroundLogger and its clients. (Since that’s a reasonable internal implementation strategy, though, that’s how I model things in the new PlaygroundLogger implementation.)

So if PlaygroundLogger isn’t going to accept raw `Data` values as input, it needs to take *something*. I don’t think it makes a ton of sense for PlaygroundLogger/PlaygroundSupport to define a bunch of intermediate types, nor do I think it is PlaygroundLogger/PlaygroundSupport’s role to define protocols for what it means to be an image, color, view, integer, etc. (Perhaps that’s the sticking point here?) And even if it did provide those protocols, there’d be an issue surrounding how PlaygroundLogger would choose an implementation if a type implemented multiple of these core protocols. (I discuss that briefly as a rejected alternative, as my first thinking for this problem was that I should introduce a suite of protocols like `CustomNSViewConvertible`, `CustomIntConvertible`, `CustomUIColorConvertible`, and so on.)

I don’t think it needs to define protocols for what it means to be an image, but it could define a general “playground log formatter” abstraction which can translate an object in to a log entry (with a default implementation based on Mirror).

That could be a way to solve the problem above. The core types could simply expose formatter objects, and CustomPlaygroundRepresentable could just ask each instance explicitly for the formatter it wants to use (falling-back to the Mirror-based one if the custom formatter fails or the object asks explicitly for no special formatting).

Previously, the standard library and PlaygroundLogger solved this with the PlaygroundQuickLook enum. For the reasons listed in my proposal, I think that the PlaygroundQuickLook enum is harmful, so I wouldn’t want to solve this problem that way.

So given that, I think it’s not unreasonable for a platform/toolchain’s playground logger to know about that platform/toolchain’s important types so it can produce the appropriate opaque log entries. If there’s some approach that I’m overlooking — or if I’m dismissing the suite of protocols case too quickly — I would love to hear it, because I’m open to suggestions.

Lets say we have something like a formatter abstraction:

/* sealed */ protocol PlaygroundLogEntry { }

// We can expose concrete implementations of PlaygroundLogEntry as we like. We might also have private entry formats.
// It might also be an enum with private cases, when we get that feature.

protocol PlaygroundFomatter {
    func format(object: Any) -> PlaygroundLogEntry? // may fail if object type is unexpected.
}

protocol CustomPlaygroundRepresentable {
    var playgroundFormatter: PlaygroundFormatter? { get } // if nil, uses the minimal, Mirror-based formatter.
}

PlaygroundLogger/Support can build in some basic formatters for stuff like some PNG-formatted bytes, RGB colours, OpenGL textures. Stuff which is reasonably useful across platforms.

struct PNGImageFormatter: PlaygroundFormatter {
    func format(object: Any) -> PlaygroundLogEntry? {
        guard let imageData = object as? Data else { return nil } // unexpected type.
        // Parse the image data and return something the playground can understand.
    }
}

But it might also provide some optimised formatters for OS types, like UIView.

struct UIViewFormatter: PlaygroundFormatter {
    func format(object: Any) -> PlaygroundLogEntry? { /* magic */ }
}

extension UIView: CustomPlaygroundRepresentable {
    static var defaultPlaygroundFormatter: PlaygroundFormatter { return UIViewFormatter() }

    var playgroundFormatter: PlaygroundFormatter? { return UIView.defaultPlaygroundFormatter }
}

Also, one more handy formatter:

struct KeyPathFormatter<T>: PlaygroundFormatter {
    let base: PlaygroundFormatter
    let dataKP: KeyPath<T, PlaygroundFormatter>

    func format(object: Any) -> PlaygroundLogEntry? {
        return base.format(object: object[keyPath: dataKP])
    }
}

Then, when you’re porting the rich graphical previews to a new platform, you can map your base-level UI objects in terms the formatter knows about, or write your own for any public log-entry formats (if there are any).

class OpenGLView {
    var formatter: PlaygroundFormatter? {
        return KeyPathFormatter(base: PlaygroundSupport.OpenGLTextureFormatter(format: self.textureFormat), keyPath: \.backingTexture)
    }
}

This way, sure, PlaygroundLogger/Support has some built-in behaviours for OS types it knows about. That said, I suppose it would be open for anybody to add their own if it’s useful for the community. We don’t have a way in Swift yet to make sealed protocols, but an enum with private cases might allow us to model this well. Nobody is going to switch over it, so it should be fine :P

I think this is a potentially interesting direction for exploration of how to expose hooks for PlaygroundLogger in the future. But as noted in my earlier reply to Chris, I think that the style of API I’m proposing here is still useful for the simple case, even if we want to expose this more complex support someday in the future.

Connor

···

On Jan 13, 2018, at 8:20 AM, Karl Wagner <razielim@gmail.com> wrote:

On 13. Jan 2018, at 03:22, Connor Wakamo via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Jan 11, 2018, at 11:51 PM, Chris Lattner <clattner@nondot.org <mailto:clattner@nondot.org>> wrote:
On Jan 11, 2018, at 11:22 AM, Connor Wakamo <cwakamo@apple.com <mailto:cwakamo@apple.com>> wrote:

Semi-related to this proposal, I’m working on a rewrite of the PlaygroundLogger library (currently at <https://github.com/cwakamo/swift-xcode-playground-support/tree/runtime-framework-and-improved-logger&gt;\) which makes it so that the only special standard library behavior it depends on is Mirror — it no longer relies on the Swift runtime (via PlaygroundQuickLook(reflecting:)) to figure out what to log, instead opting to check protocol conformances itself. So if this proposal is accepted into Swift, concurrent with that we’ll have a new PlaygroundLogger implementation which gets rid of as many hacks as possible.

****awesome****

I hope that someday when we have a better API than Mirror (e.g. that supports mutation) that allows reflecting on stored properties, methods, and all of the other things that Swift needs to eventually support that you’ll switch to it. I understand that the glorious future isn’t very helpful to you today though.

Yes, I very much look forward to being able to migrate off of Mirror when that time comes.

`CustomPlaygroundLoggable` would be a little clunkier to implement than `CustomPlaygroundRepresentable` is, as in the common case folks would have to write `return .custom(…)`. It’s possible that the clarity and additional flexibility this grants outweighs that cost; I’m not sure, and would love feedback on that.

I just don’t understand the usecase for “conditional customizing” at all. By way of example, we don’t have the ability to do that with CustomStringConvertible. What is different about this case?

I think the big difference with CustomStringConvertible is that it’s possible for a conformance to reimplement the default behavior on its own. For instance, if I have:

  enum Foo {
    case one(Any)
    case two
  }

As noted above, recovering the default behavior with CustomPlaygroundRepresentable is not always possible if the return type is `Any`. That very well might be an acceptable trade-off to keep the API simple.

Why can’t an implementation just return “.two” instead of nil?

Is the problem the “one” case? If that is the problem, then it might be better to take a completely different approach where you embrace the fact that you have structured data producing 2D results that need to be displayed. Each result could either return an atomic result or a result that wraps some other recursive 2D presentation.

No, the issue is that the playground logger would see that `Foo.two` conforms to `CustomPlaygroundConvertible`, and would therefore call `playgroundDescription` another time, which would return another `Foo.two`, and this would continue endlessly. I plan to guard against that in PlaygroundLogger with a limit on chaining, but this protocol should not require a failsafe as a feature.

This chaining is so that `Foo` can return a value of `Bar` which itself conforms to `CustomPlaygroundConvertible` — the promise of the API is that a value will be logged as if it were the value returned from `playgroundDescription`, so that the following lines:

  let foo = Foo()
  let bar = Bar()

produce the same log output (modulo things like type name).

Fundamentally though, playground presentation is solving several different problems:

1. Types with no special behavior can always be represented as strings, which is handled by base protoocols.
2. Some types want custom string representations to show up in playgrounds, but not in “print” and string interpolation.
3. Some types want to provide a 2D “quicklook” style presentation in addition and beyond the string representation.
4. Fewer types want to provide an animated 2d representation, that is either “live” or precomputed.

I’d suggest that this protocol only tackle problems 2 and 3, but you should cleanly distinguish between them, probably with a bespoke enum or struct that models these capabilities.

This is not quite accurate. All types have both a textual presentation and a “quicklook”-style presentation. I’d characterize this as:

  1. PlaygroundLogger determines if the subject being logged conforms to `CustomPlaygroundConvertible`. If so, it calls `playgroundDescription` and starts over, treating the return value as its subject.
  2. PlaygroundLogger then produces the data necessary to construct a “quicklook”-style presentation by doing the following:
    a) If the subject is a type known to PlaygroundLogger (modeled as a conformance to a PlaygroundLogger-internal protocol), then PlaygroundLogger produces opaque log data for that type’s “quicklook”-style presentation. (As examples: for String, it encodes UTF-8 bytes; for CGPoint, it encodes the x and y points using an NSKeyedArchiver; for UIView, it encodes a rendered image; etc.)
    b) Otherwise, PlaygroundLogger constructs a Mirror for the subject and produces structured log data representing the subject for a structural “quicklook”-style presentation.
  3. PlaygroundLogger then uses `String(describing:)` on the subject being logged to produce the textual representation. (In some cases, IDEs may ignore this value in favor of textual-ish information about the “quicklook”-style presentation, such as color swatch or the dimensions of the logged image.)

PlaygroundLogger does not support producing animated representations.

The `CustomPlaygroundConvertible` protocol is all-or-nothing; if you implement it, both your textual and “quicklook”-style presentations are modified to match whatever you returned. There isn’t a facility to only affect one presentation or the other. This provides parity with `CustomPlaygroundQuickLookable` but with a more flexible interface, which is the intent of this proposal.

Connor

I do like the `playgroundDescription` name for the property, but am a little hesitant to use the name `CustomPlaygroundConvertible` because conforming types can’t be converted to playgrounds. I can’t come up with an appropriate word in `CustomPlaygroundThingConvertible` to use in place of `Thing`, though. (If we end up pivoting to the enum I described above then something like `CustomPlaygroundLoggable` would be more appropriate.)

I would strongly recommend aligning with the state of the art in CustomStringConvertible (which has been extensively discussed) and ignore the precedent in the existing playground logging stuff (which hasn’t).

That’s very reasonable. I’ll update the proposal to use CustomPlaygroundConvertible (unless I or someone else can come up with a really good “Thing” for a name like CustomPlaygroundThingConvertible, as that would even better match CustomStringConvertible/CustomDebugStringConvertible).

Thank you!

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

SGTM!

-Chris

···

On Jan 17, 2018, at 3:14 PM, Connor Wakamo <cwakamo@apple.com> wrote:

On Jan 12, 2018, at 11:02 PM, Chris Lattner <clattner@nondot.org <mailto:clattner@nondot.org>> wrote:

On Jan 12, 2018, at 6:22 PM, Connor Wakamo <cwakamo@apple.com <mailto:cwakamo@apple.com>> wrote:

The second case, and the one I’m more worried about, is subclassing. If, for instance, you have the following:

  class FooView: UIView, CustomPlaygroundRepresentable {
    var playgroundRepresentation: Any {
      return “A string describing this view"
    }
  }

  class BarView: FooView {
    override var playgroundRepresentation: Any {
      // BarView instances wanted to be logged as themselves, but there’s no way to express that
      return ???
    }
  }

There’s nothing that BarView can do to ensure it gets logged like a view because FooView declared a conformance to CustomPlaygroundRepresentable.

I really don’t understand this. FooView declares that it conforms, and it provides a “playgroundRepresentation” member. Cool for FooView.

BarView comes around and overrides FooView’s implementation. They don’t have to conform, because it *isa* FooView, so of course it conforms. If it wants to customize its presentation, it overrides its “playgroundRepresentation” method and it… just works. If it conditionally wants the FooView representation for some reason, it can even call “super.playgroundRepresentation”. What’s the problem? This seems ideal to me.

The issue is that there’s no way for `BarView` to recover the default playground representation which `UIView` and its subclasses get.

I get it now, thanks.

For a `UIView`, the PlaygroundLogger library renders an image of the view and packages that up in a log entry. If `BarView` wants that instead of `FooView`’s custom representation, there’s no way for it to request it.

I can think of two different ways to solve this problem:

1) Go with your suggestion upthread, where a custom enum is used: implementations do:

  if predicate() {
    return .custom(“my representation)
  } else {
    return .default
  }

The problem with this is that you’re forcing implementations of this to deal with the enum just because of the rare case. Not a showstopper, but doesn’t feel right.

2) Go with the simple approach of returning Any, and add a new function to the Playground API somewhere that produces the default representation that they can call, the unusual case above would look like this:

  if predicate() {
    return “my representation
  } else {
    return SomePlaygroundAPI.getDefaultPlaygroundRepresentation(self)
  }

This seems like the best of both worlds to me.

Yes, agreed. I’m not going to propose adding this to PlaygroundSupport as part of this proposal (under the assumption that this is an extreme edge case to begin with), but I’ll include a reference to it in the “alternatives considered” section so it can be referenced in the future should this be important enough that it warrants support.

IMO, every time you choose to privilege “well known” types in the standard library with special code in Xcode (or some other high level system loosely integrated with swift.org <http://swift.org/&gt;\) you are introducing technical debt into the Swift ecosystem. This is because you are violating the basic principles on which Swift was established, which is that users can [re]define primitives to build their own abstractions and carve out new domains beyond what was originally envisioned when you’re writing the UI code.

So given that, I think it’s not unreasonable for a platform/toolchain’s playground logger to know about that platform/toolchain’s important types so it can produce the appropriate opaque log entries. If there’s some approach that I’m overlooking — or if I’m dismissing the suite of protocols case too quickly — I would love to hear it, because I’m open to suggestions.

Yes, you are completely right: Unless you are willing to make the structured schema public, there is no point to doing what I suggest.

I do think that there is a schema which would make sense to take public (similar to the abstract format used in treeviews like the navigator and UI debugger for variables, strikingly similar to what JSON can express with a fancier terminal than strings), but there is no rush to do that, and it is best to take time to consider it deliberately.

Likewise, agreed — I don’t want to try to figure out what that would look like with the time pressure imposed by trying to get this in Swift 4.1 prior to ABI stability. And even in a world where this is exposed in a way to allow custom implementations at that low level, I think that a `CustomPlaygroundConvertible`-style API is still useful as the simple way of customizing display for most clients.

Connor

Is the problem the “one” case? If that is the problem, then it might be better to take a completely different approach where you embrace the fact that you have structured data producing 2D results that need to be displayed. Each result could either return an atomic result or a result that wraps some other recursive 2D presentation.

No, the issue is that the playground logger would see that `Foo.two` conforms to `CustomPlaygroundConvertible`, and would therefore call `playgroundDescription` another time, which would return another `Foo.two`, and this would continue endlessly. I plan to guard against that in PlaygroundLogger with a limit on chaining, but this protocol should not require a failsafe as a feature.

Right, it seems like the best way to solve this is to provide explicit access to the default presentation.

-Chris

Hello,

there already was some discussion about providing an explicit method to request the default (structural) representation. How about creating some wrapper type for that instead of nil or some explicit method?

class MyDerivedClass: SomeClassWithCustomRepresentation {
    var playgroundDescription: CustomPlaygroundRepresentable {
        // this class uses the default representation instead of the inherited one
        return StructuralPlaygroundRepresentation(self)
    }
}

This way, it is easily possible to delegate the representation and to avoid using Any as type for playgroundDescription. Both StructuralPlaygroundRepresentation and our standard types would implement CustomPlaygroundRepresentable (or whatever names we choose).
All types without a custom implementation would implicitly be represented by this wrapper type.

-- Martin

CustomDebugVisualConvertible.

This name suggests it should work in the debugger, but…well, does it? Shouldn't it? How does the debugger's Quick Look support work, anyway? Can you hook into it from Swift? If not, why not?

How does the debugger’s Quick Look support work, anyway?

Are you talking about LLDB? That's done using debugQuickLookObject() on any type that derives from NSObject.

Are you talking about LLDB?

Yes, that's what I'm talking about.

That’s done using debugQuickLookObject()1 on any type that derives from NSObject.

So it's not available on pure-Swift types? Should we plan to make it available for them, and would it make sense to use the same mechanism as a playground does?