71

I need to communicate some information from compile scripts into Template Haskell. Currently the compile scripts keep the information in the system environment, so I just read it using System.Environment.getEnvironment wrapped in runIO. Is there a better way, such as passing some arguments to ghc (similar to -D... for the C pre-processor), or perhaps something specifically designed for this purpose in TH?

Petr
  • 62,528
  • 13
  • 153
  • 317
  • 9
    Reading that information from an external file and using [`addDependentFile`](http://hackage.haskell.org/package/template-haskell-2.8.0.0/docs/Language-Haskell-TH-Syntax.html#v:addDependentFile) to make that file known to `ghc --make` is an obvious alternative. What are the problems that you have with the current scheme? – Mikhail Glushenkov Oct 30 '13 at 11:22
  • 2
    @MikhailGlushenkov Actually the environment passes just the root of the project directory and then more information is read from a file. So `addDependentFile` will be helpful to my case. The current scheme is working, I just wanted to know if there is some other, canonical way how to do it. – Petr Oct 30 '13 at 12:18
  • 5
    You can also use the [`location`](http://hackage.haskell.org/package/template-haskell-2.8.0.0/docs/Language-Haskell-TH-Syntax.html#v:location) function to get the root of the project directory (assuming that you know the relative path from the current module to the root). Here's [an example](https://gist.github.com/23Skidoo/6258379). – Mikhail Glushenkov Oct 30 '13 at 12:23
  • you could use -XCPP with template haskell, but it seems like your way is working better. – aavogt Oct 31 '13 at 00:57
  • @aavogt I thought about it, but I'd rather avoid CPP, since I have no other use for it. – Petr Oct 31 '13 at 07:18
  • 1
    Do you want someone to a user to select there own configuration file, for example, by passing a file path at the command line? – user3125280 Dec 24 '13 at 22:32

2 Answers2

14

Since so many people are interested in the question, I'll add my current approach, perhaps somebody will find it useful. Probably the best way would be if TH allowed to read -D parameters on GHC's command line, but it seems nothing like this is currently implemented.

A simple module allows TH to read compile-time environment. A helper function also allows to read files; for example read the path of a configuration file from the environment and then read the file.

{-# LANGUAGE TemplateHaskell #-}
module THEnv
    (
    -- * Compile-time configuration
      lookupCompileEnv
    , lookupCompileEnvExp
    , getCompileEnv
    , getCompileEnvExp
    , fileAsString
    ) where

import Control.Monad
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (Lift(..))
import System.Environment (getEnvironment)

-- Functions that work with compile-time configuration

-- | Looks up a compile-time environment variable.
lookupCompileEnv :: String -> Q (Maybe String)
lookupCompileEnv key = lookup key `liftM` runIO getEnvironment

-- | Looks up a compile-time environment variable. The result is a TH
-- expression of type @Maybe String@.
lookupCompileEnvExp :: String -> Q Exp
lookupCompileEnvExp = (`sigE` [t| Maybe String |]) . lift <=< lookupCompileEnv
    -- We need to explicly type the result so that things like `print Nothing`
    -- work.

-- | Looks up an compile-time environment variable and fail, if it's not
-- present.
getCompileEnv :: String -> Q String
getCompileEnv key =
  lookupCompileEnv key >>=
  maybe (fail $ "Environment variable " ++ key ++ " not defined") return

-- | Looks up an compile-time environment variable and fail, if it's not
-- present. The result is a TH expression of type @String@.
getCompileEnvExp :: String -> Q Exp
getCompileEnvExp = lift <=< getCompileEnv

-- | Loads the content of a file as a string constant expression.
-- The given path is relative to the source directory.
fileAsString :: FilePath -> Q Exp
fileAsString = do
  -- addDependentFile path -- works only with template-haskell >= 2.7
  stringE . T.unpack . T.strip <=< runIO . T.readFile

It can be used like this:

{-# LANGUAGE TemplateHaskell #-}
import THEnv
main = print $( lookupCompileEnvExp "DEBUG" )

Then:

  • runhaskell Main.hs prints Nothing;
  • DEBUG="yes" runhaskell Main.hs prints Just "yes".
Petr
  • 62,528
  • 13
  • 153
  • 317
  • This is good, but not perfect: Setting the environment variable will not cause GHC to recompile the module. Is that possible without changing GHC? – Joachim Breitner Apr 08 '22 at 08:49
3

It looks like what you are trying to do here, The -D option in ghc seems to define a compile time variable.

Here, on the same subject is a question that seems to also answer the other part of your question. From what I can tell, to do conditional compilation, you do something like:

    #ifdef MACRO_NAME
    //Do stuff here
    #endif
Community
  • 1
  • 1
violet_white
  • 404
  • 2
  • 15
  • 1
    As I said in the comments, I don't want to use CPP and conditional compilation. I have no use for it, all I want is to pass information to Template Haskell. The `-D` option would be nice, if there would be a way how to read it in TH without CPP. – Petr Dec 26 '13 at 14:23
  • 1
    Again, this is for conditional compilation in the *haskell* code. I don't know of anything other than defining macros with -D(maybe with some value set), and then you could check the value in your haskell and it might work. I don't know enough about haskell to be sure though. – violet_white Dec 26 '13 at 20:16