How do i actually ban a type from participating in string interpolations?

i have a type, let’s call it GitCommit, that once looked like this:

public 
struct GitCommit
{
    private 
    let sha1:SHA1
}

and conformed to CustomStringConvertible like this:

extension GitCommit:CustomStringConvertible
{
    public var description:String { "\(self.sha)" }
}

this conformance was used everywhere.

func computeURL() -> String 
{
    "https://github.com/apple/swift/\(self.commit)"
}

recently, i wanted to add another field to the type.

public 
struct GitCommit
{
-   private
+   public 
    let sha1:SHA1
+   public
+   let name:String
}

i tried to revoke the CustomStringConvertible conformance like this:

@available(*, unavailable)
extension GitCommit:CustomStringConvertible
{
    public
    var description:String { fatalError() }
}

as we should no longer be interpolating GitCommits, rather, we should be specifying one of name or sha1. but i was dismayed to find that this does not actually prevent GitCommit from appearing in string interpolations.

how do you actually ban a type from participating in string interpolations?

3 Likes

The only way I can think of is to prevent it from being wrapped in Any -- since you can always interpolate an Any -- i.e. make it ~Copyable. I presume the far-reaching consequences of doing this are unacceptable.

Similarly, you can also pass anything to String.init(describing:), which is what string interpolations default to. I think my own best suggestion is to provide an explicit appendInterpolation(_:) for it, which you then mark deprecated (not unavailable, because that would eliminate it from overload selection):

extension DefaultStringInterpolation {
    @available(*, deprecated, message: "choose between the sha and the name")
    func appendInterpolation(_: GitCommit) {
      // do the old thing for compatibility
    }
}
2 Likes

If you want to have stricter control over how your output is presented, it might be interesting to consider using your own ExpressibleByStringLiteral type instead of String.

1 Like

that strikes me as an inconvenient set of places to put this logic, because string conversion is used in a lot of diverse places in our code base. for example:

  • interpolating GitCommit.sha1 into a URL path component
  • interpolating GitCommit.sha1 into a JSON string
  • interpolating GitCommit.sha1 to some human-readable text content of an HTML node
  • interpolating GitCommit.sha1 to some terminal output

all of these sites are using string interpolations, and it doesn’t really make much sense to me, to have to introduce URL.PathComponent, JSON.StringNode, HTML.StringNode, Terminal.LogText, etc. just to deal with GitCommit.

i am actually using @available(*, deprecated) on the CustomStringConvertible conformance itself, which has the same effect. however, this is a poor workflow generally, for a task that needs to be done quite frequently as CustomStringConvertible conformances are revoked:

  • @available(*, deprecated) only raises warnings the first time the project is built. therefore, you need to do a clean build on each iteration.

  • @available(*, deprecated) does not mix well today with .enableExperimentalFeature("StrictConcurrency"), because strict concurrency produces an enormous volume of unsurpressable warnings (e.g., CommandLine.arguments) that can conceal the deprecation warnings.

  • @available(*, deprecated) is not really the correct tool for the job, as it still allows the property to be called. it has really become unavailable, and that should be enforceable by the compiler.

1 Like