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)?
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.
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.
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.
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.
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.
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)
}
}
}
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.
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.
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.
We should ensure this also applies to os_log
and friends, which uses different interpolation than general strings.