Thanks to SE-0196, Swift 4.2 introduces #warning()
and #error()
compiler directives. These will allow you to incorporate diagnostic messages and emit errors during compilation. Here are some examples from the proposal, which has already been accepted and implemented:
#warning("this is incomplete") #if MY_BUILD_CONFIG && MY_OTHER_BUILD_CONFIG #error("MY_BUILD_CONFIG and MY_OTHER_BUILD_CONFIG cannot both be set") #endif
The #error
example uses conditional compilation flags (set with a -D
option) to check whether conflicting configurations have been established for the build.
I’ve already written extensively about my dislike for screaming snake case (THINGS_LIKE_THIS
) in Swift. Inevitably, it seems, devs use screaming snake case for their conditional compilation flags, whether MY_BUILD_CONFIG
, MY_OTHER_BUILD_CONFIG
, or DEBUG
. Although an industry standard, it feels like a clash with Swift’s aesthetics.
I’ve also written about my proposal for detecting debug conditions without having to supply an explicit DEBUG
condition flag, so I’ll also leave that topic to the side for now. You can click on the link for more.
Back to topic, Swift’s newly adopted #error
and #warning
directives represents a big step up from current practices, which often rely on run-time rather than compile-time feedback.
#if !DEBUG fatalError("This code is incomplete. Please fix before release.") #endif
The unindented style in this snippet is now Swift default, avoiding minor doom pyramids for conditional compilation blocks. Even unindented, surrounding code with those conditional blocks is vertically expansive and subjectively ugly. To counter this, some coders have come up with in-line ways to force compilation (not run-time) errors with minimal condition blocks and a more succinct point-of-use approach.
Here’s an example I discovered from John Estropia. (He, in turn, cribbed it from one of his co-workers.) He uses conditional compilation to set a TODO
or FIXME
(or whatever) typealias then uses it in-line at points where a debug build should compile and release builds should error:
#if DEBUG internal typealias TODO<T> = T #endif print("Remove this later") as TODO
It’s clever. Scoping the TODO
typealias to debug builds allows lines annotated with as TODO
to throw errors during release builds. This ensures compile-time feedback at all points where a TODO
cast is performed:
error: ManyWays.playground:5:31: error: use of undeclared type 'TODO' print("Remove this later") as TODO
It’s not beautiful but it’s effective. It carries information about the call site location and the message you want to emit. If I were applying this hack, I’d probably build an actual todo
function rather than using the casting-gotcha. In the following example, I went with an upper camel case name to make the call look more directive-y and less like a standard global function. However, I drew the line at snake case:
#if DEBUG internal enum IssueLevel { case mildImportance, moderateImportance, highImportance, criticalImportance } internal func ToDo(_ level: IssueLevel, _ string: String) {} #endif // The point of use offers a compilation error, // a note, and a priority level ToDo(.highImportance, "Remove this later") // error: ManyWays.playground:13:1: error: use of unresolved identifier 'ToDo' // ToDo(.highImportance, "Remove this later")
The nicest bit is that toggling from debug compilation to release is completely automatic and centralized to a single #if
check. It’s a fascinating approach if adopted consistently and ensures that all compilation message notes like this must be resolved and removed before release.
Right now, Swift does not support a #message
directive, which performs a similar tasks. As many shops treat warnings as errors, they cannot establish a nuanced distinction between the two. If #warning
were a thing, you could use #message
to issue exactly this kind of “fix me” feedback. A further refinement, #messageOrDie
(or something like that, because naming is hard) could message for debug builds and error for release, going by whether assert
statements would or would not fire.
Dave DeLong offers another approach for structural project semantics. His introduces a Fatal
type to provide runtime cues for common development outcomes including notImplemented()
, unreachable()
, and mustOverride()
. Nothing says “you need to remember to implement this” better than a spectacular runtime crash that explains itself with full position and function context. Another cue, brilliantly named silenceXcode()
allows you to add methods that you never intend to implement and which should error if ever called.
There’s still space in Swift for expanding this metadevelopment support. I wouldn’t mind seeing both approaches added to the language: one for compile time (like #messageOrDie
) and another for run time (like Fatal
‘s namespaced static error members).
What do you think of these? And what parts of the metadevelopment process (like macros) are still MIA for you in Swift? Let me know. I’m curious to hear what else could be better established to support your development.
One Comment
I like these ideas for flagging todos. At my most recent job we were using a combination of SwiftLinter and DangerCI to forbid todo comments in pull requests. TODOs definitely become a lot more useful when you have a system for calling them out and forbidding them.
Personally I’m still skeptical of switching to a compiler error but I love the idea of finding ways to improve the feedback loop. Waiting for CI to pass is too long, though now I’m thinking a commit hook could speed that up too.
Cheers