コンパイル時にファイルからテキストを読み込む

例えばテキストを埋め込むだけなら、

main = putStrLn $(LitE . StringL <$> runIO (readFile "hoge.txt"))
-- hoge.txt の中身は hogefuga と書かれているとする。

とするだけで、

main = putStrLn "hogefuga"

と同じ意味になってくれます。
ファイルが無かったり何らかのエラーが発生したらコンパイルエラーになるだけなので安心です。


型を分解していくとこんな感じ。

readFile "hoge.txt" :: IO String
runIO :: IO a -> Q a
StringL :: String -> Lit
LitE :: Lit -> Exp

runIO で IO モナドから Q モナドに変換してくれるので、あとは Q String から文字列リテラルの Q Exp に変換してるだけです。


で、今回やりたかったのは、IP とポート番号を

127.0.0.1
8080

とか記述したファイルを読んで、(String, PortID) を返すことです。
ということでそんな感じのコードをごにょごにょ書いてみました。

ipPortFile :: FilePath -> Q Exp
ipPortFile fp = do [ip, port] <- lines <$> runIO (readFile fp)
                   return $ TupE [LitE $ StringL ip,
                                  AppE (ConE $ mkName "PortNumber")
                                       (LitE $ IntegerL $ read port)]

読んだ文字列を行単位で展開して、IP の方はさっきと同じようにそのまま文字列リテラルにしてやって、ポート番号は PortNumber コンストラクタと数値リテラルをそれぞれ作って AppE で関数適用の式を作ってやりました。
多分 Template Haskell 使わなければ

ipPortFile :: FilePath -> IO (String, PortID)
ipPortFile fp = do [ip, port] <- readFile fp
                   return $ (ip, PortNumber $ fromIntegral $ read port)

こんな感じになります。


で、あとはこれを

main = do
  handle <- uncurry connectTo $(ipPortFile "ip.txt")
  -- handle を使っていろいろする。

とかするだけです。接続先を書き換えるたびに hs ファイルを書き換える必要が無くなって幸せになれます。


C++ だと #include "ip.txt" とかするだけで……いや何でもないです。

追記

もっと超簡単に書ける方法があったようです。

ipPortFile :: FilePath -> Q Exp              
ipPortFile fp = do                           
  [ip, port] <- runIO $ lines <$> readFile fp
  let port' = read port :: Int               
  [| (ip, PortNumber $ fromIntegral port') |]
gist:1884179 · GitHub

追記2

Haskell は CPP あるんだから当然 #include もできるに決まってますよねっていう……すっかり忘れてました。