Is there any less verbose way to customize nil-valued optional types in string interpolation?

(Rick M) #1

Is there any way to make Swift smarter about my intent in situations like this? I so often have a need to write code like this:

var startTime: Date? = ...
print("Start time: \(startTime ?? "<not started>")")

But I can't do that, because startTime is Optional<Date>, not String. So I end up writing something like:

print("Start time: \(startTime != nil ? String(describing: startTime) : "<not started>")")

I'm sure I could come up with my own nil-coalescing operator to do this, and maybe that's enough, but it would be cool if I could just be expressive like this in the language. Is it too much of a hack to say "inside string interpolation, if the type of the RHS of ?? doesn’t match that of the LHS, and it is String, interpolate the LHS first, and make the entire expression type String?"

I'm not sure what kind of useful expressions this would break, but none immediately come to mind. Then again, most of you seem to be much smarter about Swift than I am.

Update:

I was able to write this:

infix operator ???

func
???(inLHS: Optional<Any>, inRHS: String)
	-> String
{
	if let lhs = inLHS
	{
		return String(describing: lhs)
	}
	else
	{
		return inRHS
	}
}

But I don't know how to make the compiler complain if the LHS isn't an Optional type.

(Joe Groff) #2

As of Swift 5, you could extend string interpolation:

extension String.StringInterpolation {
  mutating func appendInterpolation<T>(maybe: T?) {
    if let value = maybe {
      appendInterpolation(value)
    } else {
      appendLiteral("nil")
    }
  }
}

let x: Int? = nil

"\(maybe: x)"
3 Likes
(Jeremy David Giesbrecht) #3

I tend to have types themselves already pre‐loaded with easily selectable description formats. Since they are strings anyway, the issue never occurs, and the compiler will catch you if it becomes non‐optional:

extension Date {
    var inWords: String {
        return "three o’clock"
    }
    var inNumbers: String {
        return "3:00 p.m."
    }
}

print("Start time: \(startTime?.inNumbers ?? "Not started yet.")")

I usually don’t want to interpolate an unknown, arbitrary, unlocalized description anyway, since it is so often a source of UI bugs. But if you do want that, you could swallow any type by extending optional itself:

extension Optional {
    var arbitraryDescription: String? {
        switch self {
        case .some(let value):
            return String(describing: value)
        case .none:
            return nil
        }
    }
}

print("Start time: \(startTime.arbitraryDescription ?? "Not started yet.")")
(Rick M) #4

This is always in the context of debug logging, so no UI concerns.

1 Like
(Suyash Srijan) #5

Maybe the problem here is that you’re trying to shove everything into the string interpolation which is why the code looks a bit messy. Try separating your code out into variables or use an extension to provide a string value for the date. Then you can put that into the interpolation.

let startTime = Date()
let startTimeStr = startTime.stringValue ?? “nil”
print(“Start time: \(startTimeStr)”)
(Letanyan Arumugam) #6

Why not just use description explicitly?

print("Start time: \(startTime?.description ?? "<not started>")")
(Rick M) #7

That helps, I guess I thought I had to write String(describing:), which doesn't work to give you an optional result.

It's still pretty verbose when you have a lot of values to interpolate.

(Tellow Krinkle) #8

Based on this, you could also implement one with a custom fallback string like this

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

let a: Int? = 1
let b: Int? = nil
print("""
	a: \(a, or: "No a :(")
	b: \(b, or: "No b :(")
	""")