SE-0477: Default Value in String Interpolations

Huge +1 from me.

The amount of code that I've had to muddy in order to properly handle interpolation of optional values is astonishing. I'm excited to be able to refactor all of that code and get rid of the unnecessary bloat. I especially like that this works with any input type, which further reduces the amount of bloat (e.g. <value>.map { ... }) needed to get the typing to mesh property for interpolation

I’m supportive of this proposal. However, is there a reason why the default value needs to be a String? For maximum flexibility, it could accept anything, like so:

extension DefaultStringInterpolation {
    mutating func appendInterpolation<T, U>(
        _ value: T?,
        default: @autoclosure () -> U
    ) {
        if let value {
            self.appendInterpolation(value)
        } else {
            self.appendInterpolation(`default`())
        }
    }
}
3 Likes

Came here to say the same thing. No reason there should be a restriction on the default type.

I love this idea, but I think spelling it like print("Your age: \(age ?? "missing")") would be better so that it follows the pattern of when the variable is a string. It would just need an overload on the ?? operator to a type say DefaultInterpolationValue and then that has an String.StringInterpolation. appendInterpolation(DefaultInterpolationValue) function.

Also I don't think the default value necessarily needs to be a String either.

1 Like

Unfortunately the word "just" is doing a lot of lifting here. I believe that adding a new operator overload to a commonly used operator like ?? (especially with heterogeneous operands) has the potential to harm type-checking performance.

Someone more familiar with the type checker can probably weigh in with more specifics.

+1. This is a surgical fix to a pain point in string interpolation. default: seems like a reasonably good name.

My usage pretty much always uses a string literal as the default, so it wouldn't necessarily benefit from either the proposed @autoclosure() -> String or a generalized @autoclosure() -> U. I am curious if others' use case do.

2 Likes

I'm all in favor of this as a tactical solution to the optional interpolation problem. But I wanted to float a more general solution, for completeness:

The addition of an Either type into the standard library is a generally useful idea. The big win for such a nominal type is that you can give it protocol conformances for when both the underlying types conform, e.g.

enum Either<Left, Right> {
    case left(Left)
    case right(Right)
}

extension Either: CustomStringConvertible
where Left: CustomStringConvertible,
      Right: CustomStringConvertible
{
  var description: String {
    switch self {
    case let .left(l): return l.description
    case let .right(r): return r.description
    }
  }
}

This is especially useful for things like when you want to return one of two different collection types. An Either conforming to Collection is vastly more performant that any Collection or AnyCollection.

Why this is relevant here is you can define a cousin to the ?? operator, ?|, the "nil alternative operator:

infix operator ?|: NilCoalescingPrecedence

func ?|<L, R>(_ maybe: L?, _ or: @autoclosure ()->R) -> Either<L, R> {
    switch maybe {
    case let l?: .left(l)
    case nil: .right(or())
    }
}

Unlike ??, the right-hand side is not of the wrapped type, but of an arbitrary type.

This can then be used to solve things like string-interpolating an optional with a default value:

    let maybeInt: Int? = Int("x")
    print("\(maybeInt ?| "<nil>")") // prints "<nil>"

Is this better than the more targeted solution here? Probably not, but I think this kind of operator, and Either more broadly, has a lot of general utility and this is one example of that.

36 Likes

+1, we see this all the time and it’s so annoying!

+1, I too have run into this before and it seems like a very nice and unobtrusive way to improve quality of life

Thanks to all for the feedback so far! Just wanted to add a note in response to a few of the suggestions:

  • I think it would be fine to expand the default parameter to be some StringProtocol instead of String. I was a little concerned about that requiring extra work, but since StringProtocol extends TextOutputStreamable, either a string or substring's contents can just be written out to the resulting string efficiently.
  • Expanding to an unstrained, unrelated type doesn't really make sense to me, though it technically still works. In my view, the \(...) syntax is kind of like a (T) -> String function, and so it's useful to have the API show the intended purpose of the default parameter.
  • Just like Ben, I would love to have Either in the standard library – it's in the Algorithms library (internally) for these same kinds of purposes. I think it would still be a useful addition even if this new interpolation API is added, and would just be a different kind of alternative that people could use as needed. (IIRC there are some issues still to resolve with its implementation, particularly around conformances.)
9 Likes

Thanks everyone! This proposal has been accepted with the modification that Nate discusses above.

Xiaodi Wu
Review Manager

3 Likes