2016/02/29

バイナリ化したデータ構造を外部のファイルに保存して、コンパイル時にHaskellソースに埋め込む

Haskellには連想配列のように使えるMapというデータ構造(Data.Map)があります。 実行中にキーと値を更新するつもりがなく、コンパイル時に生成される固定的なMapを実行時に毎回使うという場合には、 リテラルのリストをソースコードに書いてMapに変換して使うのですが、 リストのサイズが1万要素とかになるとソース全体が肥大化してコンパイルに無駄な時間がかかるようになります。 生のリストをコンパイルのたびになめる必要はないのだから、Mapに変換したデータを辞書のように外部に持っておいて、 それをソースから読み込んで使う方法があるに違いありません。(ようするに、RubyでいうMarshal、PythonでいうPickleがしたい、ということです。)

他のファイルの内容をHaskellソースに埋め込むには、一般にTemplate Haskellが必要です。 ありがたいことに、外部のファイルの内容をソースコードに埋め込む機能に特化したfile-embedという便利なパッケージが用意されているので、これをTemplate Haskellと一緒に利用してこの問題を解決してみました。

バイナリの辞書を作る

まず、外部に保持するバイナリ辞書を下記の手順で用意します。

  1. リストからMapを作る
  2. そのMapをバイナリ化する
  3. さらにGzipで圧縮
  4. 結果を別ファイルに書き出す

コードにするとこんな感じ。このコードはバイナリの辞書(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.BinaryData.Textに対応していないためです。 また、Data.ByteString.Lazy.Char8ではバイト列がぶった切られて日本語の文字が壊れてしまうので、結果的にData.ByteString.Lazy.UTF8を使うしかありません。(ほかによい方法があったら教えてください)

バイナリ辞書をソースに埋め込んで使う

上記で生成したバイナリ辞書をdict.datのような名前でどこかに保存したら、 下記のようにしてHaskellソースに埋め込んで使います。

  1. embedFile関数にファイルを指定すると、Template HaskellのQモナドにくるまれたデータが返ってくるので、これを$(...)で取り出す(というかソースファイル中に接ぎ木する)
  2. 接ぎ木されるデータは正格なByteStringなので、fromChunksで遅延ByteStringに変換する(fromChunksはByteStringのリストをとるので、リストの文脈に入れるためにreturnしている)
  3. 圧縮された状態を解凍
  4. バイナリをほどく

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パッケージの存在を知りました。

2016/02/21

ターミナルモードのEmacsでHaskellを書いているときに補完候補をポップアップしてくれるcompany-ghc

Haskellのコードを書くときは、Windows 10からTeratermでDebianサーバにSSH接続し、そこでEmacsを-nwで起動して使っています。 標準のhaskell-modeでとくに不満はないのですが、いちおうghc-modも入れていて、単純なエラーの確認にはとても重宝しています。 でも、ghc-modの補完機能はほとんど使っていませんでした。補完のためのコマンドが手になじまず、調べている間に手打ちしてしてしまうので、ちっとも身に付かなかったからです。

とはいっても、ある程度の量を書いていると、やはり手で打っているのではしんどくなってきます。何とかならないかなーと思っていたところ、company-ghcというものを発見しました。

Company GHC
https://github.com/iquiw/company-ghc

ターミナルで開いているEmacsでも、統合開発環境のような補完メニューを実現するcompany-modeというツールがあって、そのHaskell版をghc-modを介して実現するためのパッケージらしい。試しにインストールしてみたところ、こんなふうに、まるで統合開発環境のようになりました。これならぼくにも使えそう。

インストールと設定の概略

すでにghc-modを入れているなら、そのインストール時にEmacsのパッケージをMELPAから取得できる状態にしていると思います。その場合は、M-x package-installcompany-ghcを指定するだけで、必要なパッケージを含めてインストールしてくれます(したがって事前にcompany-modeを個別にインストールする必要もありません)。MELPAからパッケージを取得する設定にしていない場合は、init.elかどこかに以下を設定します。

(require 'package)
(add-to-list 'package-archives
  '("melpa-stable" . "http://stable.melpa.org/packages/") t)
(package-initialize)

無事にインストールできたら、まずはcompany-modeを有効にします。haskell-mode利用時のみ有効にすることも可能ですが、せっかくなのでグローバルに有効にしておきます。そのほうがCtrl-c Ctrl-lでGHCiを起動したときにも補完が効くようになるし、いろいろ便利なはず。

(require 'company)
(global-company-mode)
あとは下記のようにcompany-ghcを設定すれば完了です。
(add-to-list 'company-backends 'company-ghc)
(custom-set-variables '(company-ghc-show-info t))

これだけでもとりあえず動くはずですが、company-modeのパラメータを少しいじったほうが使いやすそう。

(setq company-idle-delay 0.5)          ; 補完候補を表示するまでの待ち時間
(setq company-minimum-prefix-length 2) ; 補完候補の表示を開始する入力文字数
(setq company-selection-wrap-around t) ; 補完候補のスクロールが末尾に到達したら先頭に戻る

さらに、補完候補がポップアップ表示される際の背景色などを好みで設定します。下記の設定に加えて、Teratermの[ウィンドウの設定]で「256色モード(xterm形式)」にチェックが入っていて、かつサーバ側の環境変数TERMxterm-256colorに設定されていないと、とても日常の使用には耐えない悲惨なユーザインタフェースになってしまいます。

(set-face-attribute 'company-tooltip nil                  ; ポップアップ全体
  :foreground "black" :background "ivory1")
(set-face-attribute 'company-tooltip-common nil           ; 入力中の文字列と一致する部分
  :foreground "black" :background "ivory1")
(set-face-attribute 'company-tooltip-selection nil        ; 選択項目の全体
  :foreground "ivory1" :background "DodgerBlue4")
(set-face-attribute 'company-tooltip-common-selection nil ; 選択項目のうち入力中の文字列と一致する部分
  :foreground "ivory1" :background "DodgerBlue4")
(set-face-attribute 'company-preview-common nil           ; 候補が1つしかなくて自動で入力される場合
  :background "black" :foreground "yellow" :underline t)
(set-face-attribute 'company-scrollbar-fg nil             ; ポップアップのスクロールバー
  :background "Midnight Blue")
(set-face-attribute 'company-scrollbar-bg nil             ; ポップアップのスクロールバー
  :background "ivory1")

これで、最初の例のように、いい感じに補完候補が表示されるようになりました。

ちなみに、上記の色の設定例で「候補が1つしかなくて自動で入力される場合」とあるのは、下記のように候補が決定的になった状況のことです。

項目を選択した状態で[F1]を押すと、その項目のヘルプも見られます。

関係ないけど、TeXも捗る。

2016/02/09

LaTeXで連番リストに丸数字(\ajMaru)を使えたはずだけどhyperrefで困る話

LaTeXで丸数字の連番リスト環境の基本

そもそもLaTeXで丸数字を使いたいときは、otfパッケージajmacros.styの機能を使うのが一番お手軽です。 このスタイルでは、数値を指定してさまざまな囲み数字のグリフ(あれば)を呼び出すという便利な機能が提供されています。 たとえば、ふつうの丸数字であれば、\ajMaru{数値}として出力できます。

\ajMaru{1}、\ajMaru{100}、\ajMaru{101}

一方、enumerate環境でラベルのスタイルを変えたいときは、\theenumiとか\theenumiiといったコマンドを再定義します。 \theenumi\theenumiiは、それぞれ「enumi」や「enumii」という名前のカウンタに対応した「アラビア数字」に展開されるコマンドです。 その結果がenumerate環境でラベルを表示するのに使われているので、これらのコマンドの挙動を変えることで別の種類の数字を出力できるというわけです。 たとえばこんなふうにすれば、ラベルが大文字のローマ数字であるような連番環境が作れます。

\renewcommand{\theenumi}{\Roman{enumi}}
\begin{enumerate}
  \item 最初\label{A}
  \item\label{B}
\end{enumerate}
最初は\ref{A}、次は\ref{B}。

以上の知見を組み合わせれば、こんなふうにして丸数字の連番環境が作れそうです。

\renewcommand{\theenumi}{\ajMaru{enumi}}

しかしこれはうまくいきません。\ajMaruの引数は数値でなければならず、enumiはあくまでもカウンタの名前だからです。

ではどうすればいいかというと、ajmacros.styに用意されている\ajLabelというコマンドを使います。 このコマンドは、後続の\ajMaru{enumi}の中身を実際のカウンタ値として取り出してくれるように定義されています。

\renewcommand{\theenumi}{\ajLabel\ajMaru{enumi}}
\begin{enumerate}
  \item 最初\label{A}
  \item\label{B}
\end{enumerate}
最初は\ref{A}、次は\ref{B}。

hyperrefと一緒に使えなくなっているっぽい(2016年2月現在)

本当ならこれで話は終わりのはずなんですが、相互参照をクリッカブルにしてくれるhyperrefパッケージを同時に使うと話がややこしくなります。 hyperrefは、相互参照をクリッカブルにするためにカウンタ関連の機能をいろいろ勝手にオーバーライドするという鬼仕様なのですが、 ajmacros.styではそれに振り回されないように、 hyperrefを読み込んだ場合には\begin{document}の時点で\ajLabelなどをhyperref用にカスタマイズし直してくれます。

しかし、さらに厄介なことに、hyperrefはときどき内部の振る舞いをドラスティックに変更します。 そのため、せっかく再定義されている\ajLabelがまた使えなくなるという事態が2016年2月現在では発生しているようです。 具体的には、以下のような例で、上記のような事情を理解していないとちょっと追跡しにくいエラーが起きます。

\documentclass[dvipdfmx]{jsarticle}
\usepackage{otf}
\renewcommand{\theenumi}{\ajLabel\ajMaru{enumi}}
\usepackage{hyperref} % hyperrefを使う
\begin{document}
\begin{enumerate}
  \item 最初\label{A}
  \item\label{B}
\end{enumerate}
最初は\ref{A}、次は\ref{B}。
\end{document}

実行例

...
! Undefined control sequence.
\ajLabel ...abel \@arabic \else \Hy@ReturnAfterFi
                                                  \hyperref@ajLabel #1\fi {
l.17  \item 最
              初\label{A}
?

エラーが起きている\Hy@ReturnAfterFiは、かつてはhyperref.sty内で定義されて使われていたようですが、現在のhyperrefでは使われていないため、このような事態になってしまうようです。hyperrefパッケージのSVNのログを見ると、どうやら2011年4月に消えたようですね。

[hyperref-svn] oberdiek: r1291 - trunk
http://mail.gnu.org.ua/mailman/listarchive/hyperref-svn/2011-04/msg00002.html

上記のログを見る限り、oberdiekバンドルのltxcmdsパッケージで定義されている\ltx@ReturnAfterFiを使うようにしたので、\Hy@ReturnAfterFiをディスコンにしたようです。 \ajLabelの再定義では、この\Hy@ReturnAfterFiを利用しているので、上記の例がうまく処理できない結果になっているようです。

そこで、\Hy@ReturnAfterFi\ltx@ReturnAfterFiに変更するだけの以下のパッチをajmacros.sty(1.7b6)に当てて試したところ、意図した挙動に戻りました。 (\Hy@ReturnAfterFi\ltx@ReturnAfterFiの定義は同一なので当然といえば当然なのですが。。)

--- ajmacros.sty.org    2016-02-09 16:16:17.700785982 +0900
+++ ajmacros.sty        2016-02-09 16:15:57.548561902 +0900

@@ -685,7 +685,7 @@
 %      \def\check@UTF##1##2##3{\ifx\UTF##1\0x##2\else##3\fi}}{}}
 \gdef\ajRedefine@ajCommands{\@ifpackageloaded{hyperref}{%
        \let\hyperref@ajLabel\ajLabel
-       \def\ajLabel##1##{\ifHy@pdfstring\Hy@ReturnAfterElseFi\hyperref@ajLabel\@arabic\else\Hy@ReturnAfterFi\hyperref@ajLabel##1\fi}%
+       \def\ajLabel##1##{\ifHy@pdfstring\Hy@ReturnAfterElseFi\hyperref@ajLabel\@arabic\else\ltx@ReturnAfterFi\hyperref@ajLabel##1\fi}%
        \ajRedefine@ajCommand\"${Lig"$}\"&{Lig"&}\!*{Lig>.}\ajLig{Lig}\ajPICT{PICT}\"({PICT}\ajVar{Var}\@nil\@nil
        \aj@Redefine@ajCommand!{{Maru}!|{KuroMaru}""{Kaku}"#{KuroKaku}!~{MaruKaku}"!{KuroMaruKaku}\@nil\@nil
        \def\!J##1!K{\ifHy@pdfstring(##1)\else\expandafter\ifx\csname ajLig(##1)\endcsname\relax\@ajnumber{##1}{Kakko}%

hyperrefは、電子書籍をLaTeXで制作するうえでは唯一無二のパッケージなのですが、ときどきこういう面倒が起きるのがつらい。。

まとめ

またhyperrefか。