Haskellには連想配列のように使えるMapというデータ構造(Data.Map
)があります。
実行中にキーと値を更新するつもりがなく、コンパイル時に生成される固定的なMapを実行時に毎回使うという場合には、
リテラルのリストをソースコードに書いてMapに変換して使うのですが、
リストのサイズが1万要素とかになるとソース全体が肥大化してコンパイルに無駄な時間がかかるようになります。
生のリストをコンパイルのたびになめる必要はないのだから、Mapに変換したデータを辞書のように外部に持っておいて、
それをソースから読み込んで使う方法があるに違いありません。(ようするに、RubyでいうMarshal、PythonでいうPickleがしたい、ということです。)
他のファイルの内容をHaskellソースに埋め込むには、一般にTemplate Haskellが必要です。
ありがたいことに、外部のファイルの内容をソースコードに埋め込む機能に特化したfile-embed
という便利なパッケージが用意されているので、これをTemplate Haskellと一緒に利用してこの問題を解決してみました。
バイナリの辞書を作る
まず、外部に保持するバイナリ辞書を下記の手順で用意します。
- リストからMapを作る
- そのMapをバイナリ化する
- さらにGzipで圧縮
- 結果を別ファイルに書き出す
コードにするとこんな感じ。このコードはバイナリの辞書(Map)を生成するだけなので、コンパイルと実行はきっかり一回だけです。
▼ mkdict.hs
{-# LANGUAGE OverloadedStrings #-} import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Lazy.UTF8 as BSLU import qualified Data.Map as Map import Codec.Compression.GZip (compress) import Data.Binary (encode) dictList :: [(Int, BSLU.ByteString)] dictList = zip [1..] $ (map BSLU.fromString ["西住", "武部", "秋山", "五十鈴", "冷泉", ...その他大勢 ] main = do BSL.putStr $ compress . encode . Map.fromList $ dictList return ()
ここでのポイントは、Mapのもとになる連想リストに日本語などの多バイト文字列がある場合、Data.Text
ではなくData.ByteString.Lazy.UTF8
を使うことです。
なぜData.Text
が使えないかというと、バイナリ化に使うData.Binary
がData.Text
に対応していないためです。
また、Data.ByteString.Lazy.Char8
ではバイト列がぶった切られて日本語の文字が壊れてしまうので、結果的にData.ByteString.Lazy.UTF8
を使うしかありません。(ほかによい方法があったら教えてください)
バイナリ辞書をソースに埋め込んで使う
上記で生成したバイナリ辞書をdict.dat
のような名前でどこかに保存したら、
下記のようにしてHaskellソースに埋め込んで使います。
embedFile
関数にファイルを指定すると、Template HaskellのQモナドにくるまれたデータが返ってくるので、これを$(...)
で取り出す(というかソースファイル中に接ぎ木する)- 接ぎ木されるデータは正格なByteStringなので、
fromChunks
で遅延ByteStringに変換する(fromChunks
はByteStringのリストをとるので、リストの文脈に入れるためにreturn
している) - 圧縮された状態を解凍
- バイナリをほどく
▼ getmessage.hs
{-# LANGUAGE TemplateHaskell #-} import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Lazy.UTF8 as BSLU import qualified Data.Map as Map import Data.FileEmbed (embedFile) import Codec.Compression.GZip (decompress) import Data.Binary (decode) dictMap :: Map.Map Int BSLU.ByteString dictMap = decode . decompress $ BSL.fromChunks . return $ $(embedFile "data.dat") getMember id = putStrLn $ case Map.lookup id dictMap of Just x -> BSL.toString x Nothing -> error $ "No entry for " ++ id
実行結果
$ ghc mkdict.hs -o mkdict [1 of 1] Compiling Main ( mkdict.hs, mkdict.o ) Linking mkdict ... $ ./mkdict > data.dat $ ghci GHCi, version 7.10.3: http://www.haskell.org/ghc/ :? for help Prelude> :l getmember.hs [1 of 1] Compiling Main ( getmember.hs, interpreted ) Ok, modules loaded: Main. *Main> getMember 3 秋山 *Main> getMember 5 冷泉 *Main>
参考資料
- Compiling in constants(Haskell Wiki)
- Gzipしたバイナリデータのリテラル表現をソースに埋め込んでいる事例(そんな人間が見ても意味がないデータをソースに貼り付けるのはいやだ)。 バイナリ辞書をCに変換して利用する例も紹介されているけど。
- Haskellでバイナリデータ(yuga/gist:8255552)
Data.Text
に対応したバイナリライブラリがなさそうなんだなということが分かりました。- How to compile a resource into a binary in Haskell?(Stack Overflow)
file-embed
パッケージの存在を知りました。