RE: #ifdef considered harmful (was: DData)

Robert Will writes:
I think what Haskellers (and others) can do in most cases is to factor out platform dependent parts to mini-modules which have different implementations but the same interface. The version of such a module is choosen by the path it resides in during the build process. (Simplest case: the -i option of the compiler.)
I have to say, I dislike this approach to conditional compilation. - it puts the conditional in the build system rather than the source code, which means you have to look in the build system to find out what the code means. - it means if I want to ask questions like "hmm, how does this differ from the Windows version?" I have to go find the windows version of the file. - the interface to the module is duplicated, and nothing is checking that all your implementations have the same interface. OTOH, if you want to see a good example of #ifdefs gone wrong, take a look at GHC's native code generator. #ifdef can indeed be misused; that's not a reason to throw it out altogether. Cheers, Simon

On Thu, 15 Apr 2004 11:13:34 +0100, Simon Marlow
Robert Will writes:
I think what Haskellers (and others) can do in most cases is to factor out platform dependent parts to mini-modules which have different implementations but the same interface. The version of such a module is choosen by the path it resides in during the build process. (Simplest case: the -i option of the compiler.)
I agree with you that in general we should avoid a preprocessor. However, in the DData case it is hard to see a different solution. Using a "mini module" to do the bit-fiddling won't work that well since 1) It is imperative for performance that the ghc "shiftRL#" gets inlined, which doesn't happen if it is in a separate module. 2) As part of the standard libraries, you can't have three extra modules for each system I don't say that I *like* to use cpp here, but it is surely the easiest solution here -- not perfect, but the perfect solution would take an inappropiate amount of resources. All the best, Daan.

--- Daan Leijen
I agree with you that in general we should avoid a preprocessor. However, in the DData case it is hard to see a different solution. Using a "mini module" to do the bit-fiddling won't work that well since
1) It is imperative for performance that the ghc "shiftRL#" gets inlined, which doesn't happen if it is in a separate module.
Is this true? I thought GHC exports some intermediate code in .hi files.
2) As part of the standard libraries, you can't have three extra modules for each system
Yet, in this case, maybe the various haskell implementations could be unfied in a manner that #ifdefs become unnecessary. Also, I think that support for older implementations (ie. GHC < 503) can be dropped, but I need an "official" opinion here. Cheers, JP. __________________________________ Do you Yahoo!? Yahoo! Tax Center - File online by April 15th http://taxes.yahoo.com/filing.html

Daan Leijen wrote:
I agree with you that in general we should avoid a preprocessor. However, in the DData case it is hard to see a different solution. Using a "mini module" to do the bit-fiddling won't work that well since
1) It is imperative for performance that the ghc "shiftRL#" gets inlined, which doesn't happen if it is in a separate module. 2) As part of the standard libraries, you can't have three extra modules for each system
I don't say that I *like* to use cpp here, but it is surely the easiest solution here -- not perfect, but the perfect solution would take an inappropiate amount of resources.
Hmm, wouldn't it be nice to have a "FastInt" library which re-bound the appropriate types and functions? Haskell libraries are chock-a-block with instances of "#ifdef __GHC__" which define things in terms of Int# instead of Int. We could simplify all those libraries if we did it just once, in one place. The problem, of course, is that the naive non-GHC user could write a library which used "FastInt" polymorphically, and write code that wasn't portable to GHC. I don't think there's an easy, magic solution there. Perhaps such a library would be named FastIntWhichIPromiseToTestWithGHC. -Jan-Willem Maessen
All the best, Daan.
_______________________________________________ Libraries mailing list Libraries@haskell.org http://www.haskell.org/mailman/listinfo/libraries

Haskell libraries are chock-a-block with instances of "#ifdef __GHC__" which define things in terms of Int# instead of Int.
I think the correct fix in this case is for Hugs and NHC to implement Int#. The representation of Int# as an unboxed object isn't so critical but its strictness properties are exactly what you need in a bunch of circumstances: - counters threaded through a monad - line counters in parsers/preprocessors - column numbers/widths in pretty-printing libraries What I'd like to see is a general facility for defining strict types. That is, types whose values will never be thunks so let binding causes evaluation (as in ML) and use as a subexpression (which is not under a lambda) causes evaluation (as in ML). The GHC syntax seems too restrictive for this. Maybe something like this: strict_data Int# = Int# Int -- declare a strict Int type The typechecking issues would be the same as for GHC's unboxed types: there are two kinds of typevar * and # and never the twain shall meet. (I think GHC plays some neat tricks to ease the pain of this restriction?) A semantically correct (but maybe a bit slow) implementation would be easy enough: 1) let-bind all strict non-atomic subexpressions. 2) float strict variable bindings out to nearest enclosing lambda (float out any lazy variable bindings if required) 3) Use 'seq' to force all the strict var thunks. For example: insert :: a -> (Int#,[a]) -> (Int#,[a]) insert x (cnt,xs) = (cnt+1,x:xs) ===> insert x (cnt,xs) = let cnt'=cnt+1 in seq cnt' $ (cnt',x:xs) -- Alastair Reid

Am Donnerstag, 15. April 2004 12:13 schrieb Simon Marlow:
Robert Will writes:
I think what Haskellers (and others) can do in most cases is to factor out platform dependent parts to mini-modules which have different implementations but the same interface. The version of such a module is choosen by the path it resides in during the build process. (Simplest case: the -i option of the compiler.)
I have to say, I dislike this approach to conditional compilation.
- it puts the conditional in the build system rather than the source code, which means you have to look in the build system to find out what the code means.
If the places for the modules for the different platforms are standardized then you don't have to look into the build system, do you?
- it means if I want to ask questions like "hmm, how does this differ from the Windows version?" I have to go find the windows version of the file.
Is this a problem? The very big problem with the preprocessor approach is, in my opinion, that it's awkward to add a new platform. With Robert's approach you just add some new files and don't touch the existing code. With the preprocessor approach you need write access to the source code of the library in question, I'd say.
- the interface to the module is duplicated, and nothing is checking that all your implementations have the same interface.
That's a problem, of course.
[...]
Cheers, Simon
Wolfgang

Is this a problem? The very big problem with the preprocessor approach is, in my opinion, that it's awkward to add a new platform. With Robert's approach you just add some new files and don't touch the existing code. With the preprocessor approach you need write access to the source code of the library in question, I'd say.
Which do you add more often, a new platform or a new function? Robert's approach makes the first easy and the second hard; the preprocessor makes the second easy and (you claim) the first hard. The right choice is clear, then: if you add new functions to your API more often than new platforms, use the preprocessor; if you add new platforms more often than new functions, use Robert's approach. In any case, I think thatwith well-designed macros, preprocessing allows new platforms to be handled in a localised and easy way in most cases. --KW 8-)

On Fri, 16 Apr 2004, Keith Wansbrough wrote:
Which do you add more often, a new platform or a new function?
It's not that simple! The real advantage of the ifdef-less approach is that you start creating an interface between your program and the platform. That is a unique benefit for the program's design. If you add a new function (or remove a bug, by the way), are you the super-hero that can write perfect code for all currently implemented platforms? Or would you prefer that you have that small interface and good chances that you can solve your problem by calling its functions. Otherwise you'll be happy to consult the adaptor-module's maintainer to find a proper solution. In a perfect world, everyone does his best to avoid incompatibilties. In our world, you have to be a super-hero to write good code... Robert

On Friday 16 April 2004 13:25, Wolfgang Jeltsch wrote:
The very big problem with the preprocessor approach is, in my opinion, that it's awkward to add a new platform. With Robert's approach you just add some new files and don't touch the existing code. With the preprocessor approach you need write access to the source code of the library in question, I'd say.
I find that it works a little differently in practice (when writing C, at least): 1) You write the code for one platform. 2) To add a second platform, you refactor the code by designing a portable interface to the functionality of the two platforms and splitting the code into a portable top half and two platform specific bottom halves. [In practice, the first two steps are often combined since many of us develop for two platforms at once.] 3) You add a few minor variations on the original platforms with little effort. For very closely related platforms (Win98 and Win2K, Debian and Redhat, versions 3.4 and 3.5 of <whatever>), you might have just one or two lines of changes. Suppose you need to change just one line out of 100 lines of code, do you further subdivide the platform-specific module, do you use ifdef or do you cut and paste 100 lines of code. [Complication: the person doing the port might not have access to the original platform so they can't test the modified code. No problem if you're writing a completely fresh implementation but a major issue if you have to refactor an existing implementation.] 4) You try to port it to a less familiar platform: Mac, HPUX, etc. which views the world from a different angle from Unix and Windows and have to change the design of the portable interface to all the implementations. This requires that you change the code in all implementations. [Complication: again, you may not have access to the other platforms.] Refactoring the code into multiple modules can play a part in porting code but conditional compilation plays an important role too. In particular, if you find yourself changing code that you can't test, you can use conditional compilation to leave the bulk of the code unchanged so that simple tools (e.g., diff) can help you hand-check the changes you can't check. Of course, we don't see this in Haskell much but that's because we don't try very hard to support multiple versions of a library... -- Alastair Reid
participants (8)
-
Alastair Reid
-
Daan Leijen
-
Jan-Willem Maessen - Sun Labs East
-
JP Bernardy
-
Keith Wansbrough
-
Robert Will
-
Simon Marlow
-
Wolfgang Jeltsch