I recently wanted to start covering start writing all my new app code in Swift, but hit a big problem: I use CocoaLumberjack everywhere and its not ready for Swift yet. They are working on this right now for a 2.0 release.
I checked around github and it seems some good work is being done on this. However the missing piece was very important: because Swift lacks conditional defines and a macro preprocessor, all arguments and strings you pass to the log functions would be evaluated every time — even if you had the logging level set to exclude it, or totally disabled.
So the challenge is to come up with a Swift function that can take variadic argument lists, as DDLogXXXXX functions all support formatting arguments after the initial message argument — while only evaluating the string and arguments if logging is going to happen.
Like most people I am pretty new to Swift, but have a long history with Groovy which has many similarities. I remembered reading about @autoclosure
in the Swift book, whereby parameters annotated with this will not be evaluated until your function calls the automatic closure (affects non-literal values primarily). This was the starting point, and solves the message problem immediately:
In that code, if you set debugEnabled to false in a playground, you will immediately see the output disappear, including the “I’m expensive you know” that shows you when the code is actually executing.
The next part, supporting a varargs (variadic) list for values to pass to string formatting for output, is a bit more complicated.
After quite some digging — this stuff does not appear to be documented in any significant way — I found a solution. The difficulties arise from the following:
- the C varargs/
va_list
convention used by Objective-C methods such as[NSString stringWithFormat:args:]
is inherently anathema to Swift as it deals with pointers to nasty things, but Swift pragmatically supports these through some obscure APIs like CVarArgType @autoclosure
has to be specified on the type of a parameter to a function, and I could not find a way to apply this to variadic ellipsis (…) arguments to result in a single closure that would return an array of args
What I hit upon is a solution that gets an array of closures passed as args into our function, and from there we can map that to an array of CVarArgType
which is what we need to pass to withVaList
to get the list. We do this by calling each closure in the list to get the argument that is has implicitly wrapped up.
The final hurdle then is to call the string formatting code from Objective-C using this va_list
. The withVaList
function takes a function with only a single va_list
argument, so I had to curry a function so that we had a nested function with only a single va_list
argument, which captures the messageFormat
template to use.
Again, if you run this in a playground and set debugEnabled
to false
you will see no output — most importantly none of the “EXPENSIVE” output.
I’m sure there’s a more optimal way to do this, but my Swift fu is not fully developed at this time. There is some overhead even if logging is disabled, in terms of wrapping up the arguments in auto closures and calling out to the logging function — whereas in Objective-C there’s nothing because the macros remove all the code from the compiled code. It may be that the auto closure overhead is in some cases worse than evaluating the parameter all the time — somebody with more knowledge of Swift optimisation will be able to tell us more.
If you have any improvements on it please let me know via twitter @transition_io