Edit (2016-05-18): Use featurep to improve readability in run time check. Thanks to @ogamita for the pointer.

Edit (2016-05-21): Fix spelling mistakes. featurep not featuresp. Thanks to @ngnghm for pointing that out.

How do I conditionally include code?

When code must change behaviour based on build time settings people often reach for the conditional reader macros (#+ and #-).

These macros have two properties one must be aware of.

  1. The compiler sees different code based on the condition.
  2. The macros are only evaluated at compile time.

Another thing to be aware of is that ASDF only recompiles files when their modification timestamps have changed. This is an optimization to decrease compilation time.

There are two situations where conditional code inclusion are most often used. The first is for writing code which is portable between different computing environments and the second is for setting behaviour options at build time.

In the first scenario the same source file must work on different platforms (e.g. 32 bit and 64 bit) and different compilers. The variance in target environments makes it a necessity to present different code to the compiler based on the environment. Once the file is compiled there is no reason to recompile it until it is moved to a new environment. For this scenario the two properties above (and ASDF’s partial compilation) is exactly what is needed and the correct solution is the #+ and #- macros.

The second scenario happens when the application’s behaviour can be modified by setting appropriate variables at build time. This technique if often used to switch a code base between development and production modes.

If conditional macros are used to perform this task it is easy to end up with files which were compiled under different conditions. This will almost certainly result in transient bugs, i.e. bugs which disappear when the complete project is recompiled.

Since a complete recompile avoids transient bugs the next logical step is to do a complete recompile every time strange behaviour is encountered. Doing such a recompile negates much of the benefit of ASDF’s partial compilation.

Another issue is that compiling different code based on the environment means that you can never test the complete code base in a single environment. One problem with this is that there is always uncertainty about the source of a bug which is present in only one of the environments. Another problem is that one can get into a situation where buggy code is only ever present in an environment with no debugging facilities1.

In summary, using conditional macros to implement build time settings have the following problems2:

  • Transient bugs,
  • Long compile times, and
  • Multiple execution environments.

These problems can be avoided while keeping the build time settings by using run time checks instead of compile time checks. The code examples below illustrate both the compile time and run time methods.

Conditional behaviour using reader macros

This method causes trouble.

(pushnew :app-release *FEATURES*)

#+app-release
(do-release-stuff)
#-app-release
(do-dev-stuff)

Conditional behaviour using run time checks

One possible solution to rid your code of conditional macros.

(pushnew :app-release *FEATURES*)

(if (uiop:featurep :app-release) 
    (do-release-stuff)
    (do-dev-stuff))

Conditional reader macros or run time detection?

Though I have not seen much discussion about this topic, I have seen a few projects which implement build time settings as I suggest in this post.

Method Use case
#+ and #- The same functionality is implemented by different pieces of code for different environments.
Run time detection. Different behaviours are selected based on build time options.

  1. An example is to move between SLIME and Buildapp with debugging disabled. 

  2. Also see Buildapp fails when using uncompiled libraries for another problem caused by conditional macros.