Handling platform- or configuration-specific code (or, my CPP complaints)

Good evening, cafe, Having recently taken on maintenance of a package that depends on template-haskell, I've been in some discussion with users and dependencies of my package about how best to write a library that works with multiple incompatible versions of a dependency. The two main approaches that I'm aware of are: 1. Use CPP and the macros defined by Cabal to conditionally include or exclude code for each version. 2. Use Cabal file conditionals to select hs-source-dirs containing those parts of the code (or even TH to help generate those parts of the code) that are specific to each configuration. In my discussion with others and examination of existing libraries, it seems to me that 1. is the preferred option, but I personally can think of a number of reasons to prefer the second option: * CPP was not designed for Haskell, and even cpphs still shows symptoms of it, so we have to worry about things like its handling of string gaps or single quotes. * CPP is primarily a textual manipulation tool, rather than a symbolic one, which makes it even more easy to produce malformed code with it than with TH or similar. * On that note, it's difficult to statically analyse a file using CPP: parser tools like haskell-src-exts don't support it and it's not at all obvious how or if they ever could. With Cabal choosing source files based on configuration, all the source is valid, normal Haskell and is easy for computers and humans to understand. * It may require some significant digging into each source file to establish what configurations must be tested, or to add a new supported configuration or remove an old one, if they are chosen based on CPP conditional compilation. When Cabal chooses what is compiled it is fairly explicit what is chosen and what could be, and adding a new configuration is - at least in theory - as simple as adding a new file and conditional. * It's just not very pretty! Haskell code has a sort of aesthetic that I don't think CPP macros share - different function application syntax, for example. Of course the trouble is that when your conditional compilation is on the module level there are some things which just can't be done without code duplication, and there's a tendency for small and often quite incidental bits of a function definition to suddenly be in another module instead of where they're used. So I wonder what people think of the use of CPP in Haskell code, what alternatives people can propose, or what people hope to see in future to make conditional compilation of Haskell code more elegant and simple? I once pondered whether it would be a good idea to somehow make available to TH splices information from Cabal or GHC's configuration, so that one could do something like this: myFunctionDecl = if packageVersion "template-haskell" >= Version [2, 4] [] then [d| unTyVarBndr (PlainTV n) = n; unTyVarBndr (KindedTV n _) = n |] else [d| unTyVarBndr = id |] This exactly wouldn't make sense because KindedTV wouldn't exist in the earlier version so the quotation would object, but one could imagine something similar being useful.

On 9/7/10 3:10 PM, Ben Millwood wrote:
So I wonder what people think of the use of CPP in Haskell code, what alternatives people can propose, or what people hope to see in future to make conditional compilation of Haskell code more elegant and simple?
The only thing I ever use CPP for in Haskell is top-level conditional definitions. * That is, say I have a function foo which has a vanilla Haskell definition, but also has a special definition for GHC performance hackery, or which needs a special definition on some compilers in order to correct compiler-specific bugs. I'll use #ifdef here to give the different versions. I'll also occasionally do this for things like choosing whether to use the FFI vs a native definition, for debugging purposes. * Another example is when using GeneralizedNewtypeDeriving in GHC, but still wanting to give normal definitions for other compilers to use. * The only other example I can think of is when defining Applicative instances, since I only want to do that when linking against versions of base which are new enough to have it. Occasionally you can get similar issues re ByteString vs base. In general, I think using CPP for actual macro processing is extremely poor style and can easily make code inscrutable (and no doubt bug-prone). If the Haskell spec were to add support for this sort of top-level compiler/compiletime-flag conditional definition, I'd switch over. This matches the style in most of the code I've looked at. And it also means that the incompatibilities are localized and hidden from most client code. Depending on the nature of your library API conflict, if you can localize things into a few definitions of the core functions you use in the rest of your code, then that'd be best. But that's not always possible. I've yet to run into the case where I really need to support incompatible versions of a library when it's that closely integrated, so I don't have much advice there. -- Live well, ~wren

In general, I think using CPP for actual macro processing is extremely poor style and can easily make code inscrutable (and no doubt bug-prone). If the Haskell spec were to add support for this sort of top-level compiler/compiletime-flag conditional definition, I'd switch over.
I agree that CPP used only for conditional compilation, is much more acceptable than using it for macros as well. And co-incidentally, cpphs has a --nomacro flag. :-) Regards, Malcolm

Malcolm Wallace
I agree that CPP used only for conditional compilation, is much more acceptable than using it for macros as well. And co-incidentally, cpphs has a --nomacro flag.
So that's basically what the C# preprocessor does? http://msdn.microsoft.com/en-us/library/ed8yd1ha(v=VS.100).aspx

On 09/09/2010 00:54, wren ng thornton wrote:
On 9/7/10 3:10 PM, Ben Millwood wrote:
So I wonder what people think of the use of CPP in Haskell code, what alternatives people can propose, or what people hope to see in future to make conditional compilation of Haskell code more elegant and simple?
The only thing I ever use CPP for in Haskell is top-level conditional definitions.
* That is, say I have a function foo which has a vanilla Haskell definition, but also has a special definition for GHC performance hackery, or which needs a special definition on some compilers in order to correct compiler-specific bugs. I'll use #ifdef here to give the different versions. I'll also occasionally do this for things like choosing whether to use the FFI vs a native definition, for debugging purposes.
* Another example is when using GeneralizedNewtypeDeriving in GHC, but still wanting to give normal definitions for other compilers to use.
* The only other example I can think of is when defining Applicative instances, since I only want to do that when linking against versions of base which are new enough to have it. Occasionally you can get similar issues re ByteString vs base.
In general, I think using CPP for actual macro processing is extremely poor style and can easily make code inscrutable (and no doubt bug-prone). If the Haskell spec were to add support for this sort of top-level compiler/compiletime-flag conditional definition, I'd switch over.
This matches the style in most of the code I've looked at. And it also means that the incompatibilities are localized and hidden from most client code. Depending on the nature of your library API conflict, if you can localize things into a few definitions of the core functions you use in the rest of your code, then that'd be best. But that's not always possible. I've yet to run into the case where I really need to support incompatible versions of a library when it's that closely integrated, so I don't have much advice there.
I'm afraid I instigated the CPP-vs-no-CPP issue with a patch I sent to Ben that adds backwards compatibility for GHC 6.10 to the haskell-src-meta package. Unfortunately the Template Haskell AST data type changed between the version of the library that works with 6.10 and the version that works with 6.12, and I used CPP and cabal's MIN_VERSION_* macro to conditionally compile the appropriate code. For example, unlike the previous version of the library, the AST data type for patterns in the 6.12-compatible version of the template-haskell package includes a case for bang patterns. I worked around this by using CPP to conditionally compile the case that handles translating bang patterns between the template-haskell and haskell-src-exts representations only when the new version of the template-haskell library is present. The problem with having completely separate implementations in different source code files for different versions of the template-haskell library instead of using CPP is that the rest of the pattern translation function then has to be duplicated. The question isn't whether or not to use CPP for macro expansion---I think we all can agree that that is in bad taste ;) To quote Ben, the choice is between the following two options:
1. Use CPP and the macros defined by Cabal to conditionally include or exclude code for each version. 2. Use Cabal file conditionals to select hs-source-dirs containing those parts of the code (or even TH to help generate those parts of the code) that are specific to each configuration.
I too dislike CPP, but I dislike duplicated code more. Within reason, I would prefer to be able to find a function's implementation in a single file even if that definition has a few conditionally-compiled parts. Geoff
participants (5)
-
Ben Millwood
-
Geoffrey Mainland
-
Johannes Waldmann
-
Malcolm Wallace
-
wren ng thornton