Print() for debug mode that doesn't emit code in release mode

(I feel stupid for asking this, but I think a lot of people will benefit from a solution to this problem.)

Suppose I want to have an equivalent of Swift's print() that only prints in DEBUG mode but emits zero code in RELEASE mode.

My simplest implementation for pre-macros Swift looked like this:

@inlinable
func dprint(_ s: Any) {
	assert({ print(s); return true }())
}

The use of assert() ensures no code is emitted in RELEASE mode, (edit) and that the code doesn't depend on the DEBUG conditional which may or may not be defined for various reasons.

The next iteration is to support variadic arguments along with separator and terminator (though I rarely use them if at all, but for the sake of completeness of implementation):

func dprint(_ items: Any..., separator: String = " ", terminator: String = "\n") {
	assert({
		if let first = items.first {
			print(first, terminator: "")
			for item in items.dropFirst() {
				print(separator, item, separator: "", terminator: "")
			}
		}
		print(terminator, terminator: "")
		return true
	}())
}

Seems to work fine but looks like an ugly overkill for a problem like this. Plus, it probably shouldn't be @inlinable any more which means code will be emitted in RELEASE mode.

Can this be rewritten with macros to make it look simpler and more elegant?

I'm willing to sacrifice separator and terminator but still support variadic arguments, and zero-code in release mode is important.

Edit: why assert() instead of #if DEBUG is explained in this post: Support debug-only code - #2 by Jon_Shier

I think you just need this:

func dprint(_ items: Any..., separator: String = " ", terminator: String = "\n") {
    #if DEBUG
    print(items, separator: separator, terminator: terminator)
    #endif
}

That will print items as an array, not ideal.

1 Like

You could perhaps copy the internal implementation of print into the #if block, although interestingly enough the locking is not publicly exposed.

I would be really surprised if #if DEBUG gets ignored in external modules; I'd expect packages to fully support eliminating such blocks in release builds, although I'm no expert.

Even ignoring the atomicity of one print call, which is probably not critical for debug-only printing, it's looking way too complex to implement from scratch, just check out the module that does actual printing of Any. Increasingly looking like a need for a whole framework (just for debug printing!) which honestly I'm not willing to dedicate my time to.

Also, I edited my post to include an explanation why assert() and not #if DEBUG: the latter is not guaranteed to be defined in debug builds.

To approach this from a different POV… what if you defined a com.yourcompany.yourproduct.FeatureDebug argument that can be passed in at launch (and then wrap your print statements inside a check for that flag)? There are pros and cons to both approaches… but would that be a legit option for you?

I honestly don't see what advantages that has vs. ensuring there's always DEBUG for debug builds. Also the usage of assert is not the problem here, at least it frees you from depending on conditionals and works fine.

What's truly lacking in Swift is the ability to pass varargs around as a whole.

1 Like

How about this then?

func dprint(_ items: Any..., separator: String = " ", terminator: String = "\n") {
    #if DEBUG
    print(items.lazy.map { "\($0)" }.joined(separator: separator), terminator: terminator)
    #endif
}
2 Likes

It would be nice if you could do something like this:

func dprint<each T>(_ value: repeat each T, separator: String = " ", terminator: String = "\n") {
	#if DEBUG
	print(repeat each value, separator: separator, terminator: terminator)
	#endif
}

This gives me the error Cannot pass value pack expansion to non-pack parameter of type 'Any', which implies that you can't use pack expansion to satisfy a variadic parameter. Hopefully that can be changed so these two seemingly related features can work together.

4 Likes

This is my hope for variadic splatting as well.

Yep, that's actually a pretty simple solution with a good chance of being inlined! I'd still stick to assert instead of #if DEBUG but that's not important.

Also, any particular reason for lazy here?

No, I typed it out of habit. It shouldn't make a difference here