(この記事のレギュレーション: 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.unpack
でString
を経由している。 - 準クオート構文
[|...|]
を使って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 にはもう一つ機能があって 「コンパイル時にバイナリに固定サイズのスペースを空けて、コンパイル後にそのスペースにデータを埋め込む」 というのができる。 バイナリのハッシュ値をバイナリ自体に埋め込むなどの使い方があるようだ。