Haskellの実行バイナリにファイルを埋め込む

(この記事のレギュレーション: lts-10.5)

file-embed パッケージを使う

コンパイルしてできる実行バイナリにファイルを埋め込みたいことがある。 アプリのGUIで使うアイコンとか機械学習の学習済みモデルとか。 Go では (現在では非推奨らしいが) go-bindata を使う場面だろうか。

Haskell ではそういうときには file-embed パッケージが使える。 たとえば [project root]/resources/lorem.txt に置かれた lorem ipsum テキストを埋め込む場合:

{-# LANGUAGE TemplateHaskell #-}

import Data.FileEmbed (embedFile, makeRelativeToProject)
import qualified Data.ByteString as BS

lorem :: BS.ByteString
lorem = $(makeRelativeToProject "resources/lorem.txt" >>= embedFile)

main :: IO ()
main = do
  print lorem -- > "lorem ipsum..."

$(...) の部分が Template Haskell というやつでコンパイル時に実行されるので、ファイルは実行時に参照されるのではなくバイナリに埋め込まれることになる。

自分で実装してみる

file-embed は小さいライブラリだが、もし依存関係を減らしたいなら自分で template-haskell パッケージを使って書いてしまってもよいかもしれない。 単純な実装で良ければそれほど行数を使わずに書ける。

{-# LANGUAGE TemplateHaskell #-}

import Language.Haskell.TH
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC

lorem :: BS.ByteString
lorem = $(do
             file <- runIO $ fmap BSC.unpack $ BSC.readFile "resources/lorem.txt"
             pack <- [|BSC.pack|]
             return $ AppE pack (LitE $ StringL file)
         )

main :: IO ()
main = do
  print lorem -- > "lorem ipsum..."

$() の中の実装のポイントはこんな感じ

  • Template Haskell の中では runIO で IO アクションを起こすことができ、それを使って外部のファイルを読んでいる。
  • Haskell構文木に直接 ByteString は入れ込めないので BSC.unpackString を経由している。
  • 準クオート構文 [|...|] を使って BSC.pack を Template Haskell のレベルに持ち上げている。
  • あとは AppE, LitE, StringL などの Haskell構文木を表すコンストラクタで構文木を作ればOK。template-haskell のリファレンスを読めば雰囲気は分かる。まあ間違っててもコンパイラが教えてくれるので。

ただこの単純な実装は問題があって、file-embed と違い [project root]/ が考慮されていない。 こういったプロジェクトのメタデータにアクセスするには cabal が自動生成する Paths_pkgname モジュール を使う方法があるが、これは Template Haskell では使えないようだ。 file-embed ではちょっと面白いやり方でこの問題に対処している。 ソースを読んでみると面白いかもしれない。

結論としては自前実装よりは file-embed を使うのが良いと思う。

余談

file-embed にはもう一つ機能があって 「コンパイル時にバイナリに固定サイズのスペースを空けて、コンパイル後にそのスペースにデータを埋め込む」 というのができる。 バイナリのハッシュ値をバイナリ自体に埋め込むなどの使い方があるようだ。