[Pitch] Default values for string interpolations

I see. Yes, you're right, the scope of the operator remains global unless we bake it into the compiler

Also, you'd still want rhs to be an @autoclosure.

Right, I don't think special-casing the operator logic for this small case makes sense, especially to enable a non-standard behavior of an existing operator.

Good point! I'll add it to the list.

It seems like the point of the protocol-based system to allow this kind of general addition, though I wouldn't argue super strongly for the protocol extension. I only ever really use string interpolations when creating a String, so I'd prefer to hear from users of other string-interpolatable types before proposing that particular solution.

Yep. The need to 1) make a compiler change and then 2) to explain people that it works only in string interpolation context is a big deterrent. Besides I like the potential future extensibility of the pitch approach, e.g.:

"\(value, default: "absent", width: 20, alignment: .right, placeholder: "_")
// "______________absent"

Shouldn't

    mutating func appendInterpolation<T>(_ : T?, default:)

be a customisation point (a protocol requirement fulfilled with the default implementation as in the pitch)?

1 Like

Definitely in favor of this pitch. I’ve called this appendInterpolation(_:or:) before, but I wasn’t very satisfied with that name—too cute.

StringInterpolationProtocol does not formally require a specific signature for appendInterpolation so that conformers have maximum flexibility to provide interpolations that make sense for their type. Rather than adding a formal requirement for the first time, I would recommend that we extend that approach: When the compiler is diagnosing diag::debug_description_in_string_interpolation_segment, it should check if there is an overload of appendInterpolation that matches when a default: argument label is added to the call, and if so, emit an alternate diagnostic that suggests it as the best fix.

2 Likes

Can we bikeshed the default: label? or: isn’t bad, but I was thinking ifNil:. I like that it’s both more explicit and more concise.

1 Like

As a data point, I often add Optional.unwrap(or:) to my code bases with very different meaning for "or".

extension Optional {
  func unwrap(or error: Error?) throws -> Wrapped {
    guard let self = self else { throw error ?? UnexpectedNilError() }
    return self
  }
}

Speaking as one of the pre-eminent bike-shedders of the forum… I don’t really think there’s anything to bikeshed here.

The precedent of dictionary[key, default: value] from SE–0165 is pretty strong.

27 Likes

I like this proposal as-is (though I prefer ifNil).

I agree this is targeted to print debugging, but wonder if it is thus too limited. A default can only really be used like "name: \(value, default="n/a")", when name: is always printed.

As an alternative, for interpolations I've settled on using (prefix, value, suffix, alternate). When the value is n/a, the prefix and suffix are also ignored, allowing the interpolation to compose nicely with other parts of the message.

(I admit this alternative is likely too much to bake into standard library or compiler, and relies on opaque conventions for brevity over clarity, but in case it adds to the discussion...)

I use a convention that the prefix is required, so a leading string in the interpolation always indicates an optional-handling interpolation. The prefix and suffix are unnamed, but indicated by position:

(_ prefix: S, _ value: T?, _ suffix: S = "", alt: S = "")

(where S=[String or StringProtocol])

So given

let v = someValue // describing: -> "Vv"
let ov: V? = nil

Then

Usage Result
\(ov) (handled normally)
\("", ov) (empty)
\("name: ", ov, ", ") (empty)
\("name: ", v, ", ") name: Vv,
\("pass: ", ov, alt: "FAIL") FAIL

The other variants I use are

  • closure renderer of T to String (spelled via)
  • support empty Strings and sequences as n/a
  • support Bool

Bool has no via support, but is often used with nested interpolations for prefix/suffix/alt).

So given

let es: String? = ""
let uu: [U] = [u0, u1, u2]
let eu: [U] = []
let fb = false
let tb = true

Then

Usage Result
\("name: ", es, ", ", alt: "missing") missing
\("errors: ", eu, via: { s($0) } ) (empty)
\("results:\n", eu, via: { s($0) }, alt: "FAIL" ) FAIL
\("results:\n", uu, via: { s($0) }, alt: "FAIL" ) results:\n u0 ... u1 ... u2

I appreciate that putting brevity over clarity is mostly forbidden in the Swift language and API's, but once I learned the string-prefix indicator and the prefix/value/suffix ordering, I found this clearer than the code I would otherwise have to write.

Good point, I agree that if there’s precedent we should follow it.

1 Like

I always saw that as a workaround of the limitation that custom operators currently can't be lvalues, so something like dictionary[key] ?? value += 1 is impossible to implement.

1 Like

A workaround would be to use a generic conforming to a protocol as the return value of the new operator overload:

protocol CustomOptionalCoalescingResult {
    associatedtype OptionalWrapped
    associatedtype Default
    
    init(_ optional: OptionalWrapped?, default: () throws -> Default) rethrows
}

func ?? <T>(
    optional: T.OptionalWrapped?,
    default: @autoclosure () throws -> T.Default
) rethrows -> T where T: CustomOptionalCoalescingResult {
    return try T(optional, default: `default`)
}

enum StringInterpolationOptionalCoalescingResult<T>: CustomOptionalCoalescingResult {
    case defaultString(String)
    case value(T)
    
    init(_ optional: T?, default: () throws -> String) rethrows {
        if let optional {
            self = .value(optional)
        } else {
            self = .defaultString(try `default`())
        }
    }
}

extension String.StringInterpolation {
    mutating func appendInterpolation<T>(
        _ optionalCoalescingResult: StringInterpolationOptionalCoalescingResult<T>
    ) {
        switch optionalCoalescingResult {
        case .defaultString(let string):
            appendInterpolation(string)
        case .value(let t):
            appendInterpolation(t)
        }
    }
}
2 Likes

Even if this feature existed, the default: subscript actually accesses the dictionary’s storage more directly than the normal subscript can (because it doesn’t need to move the value into an Optional so it can be nil), so the default: subscript would still be the best way to perform this operation.

If I recall correctly, ?? is already challenging to typecheck because there’s an overload with an Optional right-hand side and return value. I suspect a third overload would make things even worse.

8 Likes

I suggested it merely to support customisations by the types who want to do it differently. IIUC if it's not a protocol requirement the customisation would not work properly.

Your proposal actually can be implemented using extension to String.StringInterpolation

extension String.StringInterpolation {
    mutating func appendInterpolation<T>(_ value: T?, default: @autoclosure () -> String) {
        if let value {
            appendInterpolation(value)
        } else {
            appendLiteral(`default`())
        }
    }
}

let strValue = "VALUE" as String?
let strNil = nil as String?

print("\(strValue, default: "DEFAULT")") // "VALUE"
print("\(strNil, default: "DEFAULT")") // "DEFAULT"

let intValue = 1 as Int?
let intNil = nil as Int?

print("\(intValue, default: "DEFAULT")") // "1"
print("\(intNil, default: "DEFAULT")") // "DEFAULT"

Reference: StringInterpolationProtocol | Apple Developer Documentation

Of course it can be implemented, the question is just whether it belongs in the standard library. And personally I feel like it does.

2 Likes

Despite I personally love to tune my codebase with such sweetish improvements, I'd be super-conservative to make them part of language base.
I'm happy that Swift allows so many ways of syntax modifications and would vote for introduction of such abilities instead of convenience

@nnnnnnnn What do you think about extensibility of default argument with predefined values?

public struct StringInterpolationDefaultStringValue: ExpressibleByStringLiteral {
  internal let value: String
  public init(stringLiteral value: String) {
    self.value = value
  }
}

public mutating func appendInterpolation<T>(_ value: T?,
                                            default: @autoclosure () -> StringInterpolationDefaultStringValue) {
  if let value {
    appendInterpolation(value)
  } else {
    appendLiteral(`default`().value)
  }
}

extension StringInterpolationDefaultStringValue {
  public static let `nil`: Self = "nil"
  public static let none: Self = "none"
  // for usage inside concrete SDK
  internal static let missingValue: Self = "missingValue"
}

// Examples:

"\(optionalValue, default: "custom message")"
"\(optionalValue, default: .nil)"
"\(optionalValue, default: .none)"
"\(optionalValue, default: .missingValue)"

It is pretty annoying to repeat and copy-paste string literals for default argument.
I don't propose to add predefined values in standard library but to give people an ability to define such values in their projects.
Strings like "nil", "none", "missing value" etc. are quite repetitive.

Such design makes it unclear whether it is interpolated an optional value or not. I suggest conditional conformance of Optional to CustomStringConvertible is not implemented for similar reason, so explicit String(describing:) is required.

1 Like

We should ensure this also applies to os_log and friends, which uses different interpolation than general strings.