mixi engineer blog

*** 引っ越しました。最新の情報はこちら → https://medium.com/mixi-developers *** ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

TCのHaskellバインディングとODF繰り越し制度の紹介

はじめまして。ミュージック開発チームのtomと申します。名前はtomですが純日本人です。(本名も"tom"でちゃんと漢字があります。)

今回は、"オンラインコーヒーメーカー萌香たん"を作ったりできることでおなじみのODFをちょっとお得に使うための、「ODF繰り越し制度」の紹介と、その制度を利用して私が作っているTokyo CabinetのHaskellバインディングを紹介させていただければと思います。

ODF繰り越し制度

弊社のエンジニアは、ODF(One Day Free)という制度を使って、毎週金曜日に自分が好きなことに取り組むことができます。このODF制度、四半期ごとに実施する or しないを申告するのですが、このときになんと「繰り越す」という選択肢が用意されているのです。

普通のODFでは、四半期(3ヶ月)の間、週1日を自由な時間として確保することができます。とは言っても忙しい時期は毎週毎週時間を確保することが難しかったりします。また、1日好きなことをやっても、続きをやるまでに1週間も間があいてしまうと、集中が途切れてしまって、熱が冷めてしまったりしがちです。

そこで、ODFを「繰り越す」ということをすると、次の四半期の中のどこか1ヶ月をまるまる自分の自由な時間にすることができるのです。(3ヶ月間毎週1日を積み立てるとおおよそ12日なので、それを2回分で24日ぐらい確保できる。)One Day Freeならぬ、One Month Freeです。しかも、その1ヶ月の間は自分の好きなことに集中できるので、本当に好きなことを集中してやるには最適です。

この制度、まだまだ実験的な試みで実施者の数は多くないですが私も先月実施させていただき、1ヶ月という時間を自分の好きなことに費やすことができました。

Tokyo CabinetのHaskellバインディング

と、いうわけで作ったのがTokyo Cabinet(以下TC)のHaskellバインディングです。

あらかじめ断っておきますが、以下で紹介するライブラリは、TCの作者であるmikioさんになんの断りもなく、私個人が勝手に作ったものです。本家TCが正式にHaskellバインディングをサポートしたという話ではありません。Haskellバインディングに関する質問や不具合・バグ報告はmikioさんではなく、私の方までお願いいたします。(tom.lpsd < at > gmail.com)

インストール

PerlにはCPANという素晴らしい仕組みがありますが、Haskellにもそれに近い仕組みが用意されています。

HackageDB:?? http://hackage.haskell.org/packages/hackage.html

上記サイトに、有用なライブラリが集められています。これらは、cabal-installというパッケージに含まれているcabalコマンドを使って簡単にインストールすることができます。TCのバインディングは、tokyocabinet-haskellというパッケージ名で登録してありますので、

$ cabal update
$ cabal install tokyocabinet-haskell

とコマンドを叩けばインストールできるはずです。TCのヘッダやライブラリを特殊な場所にインストールしている場合は、

$ cabal install tokyocabinet-haskell --extra-lib-dirs=/path/to/tc/lib --extra-include-dirs=/path/to/tc/include

としてください。

使い方

TCに関する操作は、ほとんどの場合、入出力や状態変化を伴いますので、基本的にIOモナドの中で行います。 関数名やデータ型は基本的にTCのパッケージに含まれるtokyocabinet.idlに準拠しているはずです。(本家に追いついていないところも多々ありますが。。) ハッシュデータベースをいじるコード例は以下のようになります。

import Control.Monad (unless)
import Database.TokyoCabinet.HDB

main :: IO ()
main = do hdb <- new="" open="" hdb="" casket="" tch="" owriter="" ocreat="">>= err hdb
          put hdb "foo" "hop"  >>= err hdb
          put hdb "bar" "step" >>= err hdb
          put hdb "baz" "jump" >>= err hdb
          get hdb "foo" >>= maybe (error "get error") putStrLn
          iterinit hdb
          iternext hdb >>= maybe (error "iternext error") putStrLn
          close hdb
          return ()
    where
      err hdb = flip unless $ ecode hdb >>= print

new→open→ほげほげ→closeという流れは他の言語バインディングを使うのと変わりません。 BDB, FDBについてもほぼ同様に使うことができます。 まだまだ全然記述が不足しているのですが、リファレンスもあります。

keyとvalueの型は?

Haskellは静的な型を持つ言語です。TCでputやgetといった操作をするときのkey-value値にも型が必要です。型はHaskellでプログラムを書く上での主役になるので、その選択も非常に重要です。

候補1: String

はじめはkey-valueどちらもString型でよいかと考えました。String型は、Haskellで文字列を扱う際の選択肢としてもっとも気軽で使いやすいものです。多くの場合、keyとvalueは文字列として扱うことが予想されるため、これでも十分使えそうです。しかし、Stringの実体は文字列のリストです。TCに格納するためにバイト列にシリアライズする必要がありますが、この際にもオーバーヘッドが生じます。

候補2: ByteString

そこで次に考えたのがByteStringを使う方法です。bytestringパッケージに付属している、Data.ByteString.Unsafeというモジュールの関数を使うと、O(1)でCの文字列(バイト列)へシリアライズができます。これはよさそうです。 ということで、しばらくはByteStringをベースにした実装をしていました。しかしTCのtchdbaddintや、tchdnadddoubleといった文字列以外のvalue値を扱う関数を実装し始めた辺りで、ByteStringだけだとつらいかなと思うようになりました。keyやvalueの値として、IntやDoubleといった型も使いたくなったのです。

これを実現するために、いろいろと調べていくとbinaryというパッケージにたどり着きました。これを使えば、任意の型をByteStringに変換するできるため、TCのkey-value値もByteStringにしておけばいろいろうまく行きそうです。ただしこれでも少し気に入らない点がありました。これを使うコードでは、TCに格納したい値をユーザがいちいち明示的にByteStringに変換しなければならないという点です。それぐらいはラッパーを書けばなんとかなるとか、別に些細な問題だろうという見方もあると思いますが、個人的にはちょっとすっきりしないなぁと思ったのです。また、binaryパッケージが、ghcに付属のパッケージではないことも懸念点でした。binaryパッケージにたよって、key-valueの型をByteStringにすると、デフォルトのbinaryパッケージが入っていない状態ではInt型すら気軽にstoreできないからです。

Storable

とここまで悩んで、結局Database.TokyoCabinet.Storableというクラスを導入し、String、ByteString、Int、Doubleなどの型をそのクラスのインスタンスとすることにしました。こうすることで、

val :: Int
val = 100
put hdb "foo" val
-- ↑単純に 'put hdb "foo" 100' とすると100がDouble型と解釈されてしまうようなので、あえて型を明示しています
put hdb "bar" "baz"
put hdb (pack "hoge") (pack "fuga")

というように、これまで候補に挙がった型をそのままputやgetに渡すことができるようになります。

ただしこのやり方も完全ではありません。TCにputした後は、シリアライズする前の型が何だったのか、という情報が失われます。Intの値をputしたのに、getするときにString型で取り出すということができてしまいます。その結果int型をあらわしているバイト列を、無理やり文字列として読んでしまったり、ということが起こります。どちらにせよ、シリアライズされて一旦外の世界に出たデータを読むという性質上、静的な型検査で解決できる問題ではありません。ただ、こういった不整合は、発見しづらいバグを生んでしまう可能性があるので、なんとかしたいなと思っています。何か良い案があればアドバイスいただけると助かります。

TDBは?

リファレンスを見て気づかれた方もいるかも知れませんが、hackageにアップしているバージョンでは、テーブルデータベースをサポートしていません。単純にODF期間中に仕上がらなかったからという理由です。 が、その後も自分の時間を使って継続的に開発をし、githubにあがっている最新バージョンでは、TDBとTDBQRYもサポートしています。 以下のような感じでインストールしてください。(ただし絶賛開発中なので、タイミングによってはビルドに失敗したりバグっていたりするかもしれません。)

 $ git clone git://github.com/tom-lpsd/tokyocabinet-haskell.git
 $ cd tokyocabinet-haskell
 $ runhaskell Setup.hs configure --extra-lib-dirs=/path/to/tc/lib --extra-include-dirs=/path/to/tc/include --user
 $ runhaskell Setup.hs build
 $ runhaskell Setup.hs install

"検索結果に対するアトミックな更新"もサポートしています。少し長いですが以下のような感じで使います。

import Database.TokyoCabinet.TDB
import Database.TokyoCabinet.TDB.Query hiding (new)
import qualified Database.TokyoCabinet.Map as M
import qualified Database.TokyoCabinet.TDB.Query as Q (new)

data Profile = Profile { name :: String
                       , age  :: Int } deriving Show

insertProfile :: TDB -> Profile -> IO Bool
insertProfile tdb profile =
    do m <- m="" new="" put="" name="" profile="" age="" show="" just="" pk="" -="" genuid="" tdb="" main="do" ::="" io="" t="" open="" foo="" tct="" owriter="" ocreat="" mapm_="" insertprofile="" tom="" 23="" bob="" 24="" alice="" 20="" q="" addcond="" qcnumge="" setorder="" qostrasc="" proc="" cols=""> do
            Just name <- m="" get="" cols="" name="" putstrln="" put="" return="" qpput="" close="" t="" pre="">
`proc'を呼んでいる部分で、Haskellの無名関数を使って更新処理の内容を渡しています。
エラー処理などをさぼっているので、完全なサンプルではありませんが、だいたいの利用イメージを持ってもらえるのではないでしょうか。

Haskellらしくない?

ここまでのサンプルを見ると、どれもIOモナドバリバリで、Haskellの純粋関数型言語としての長所が生かされている感じがしません。ライブラリを作り始めた当初、どうせHaskellでやるのだからHaskellらしい抽象的なインターフェースを提供しようとも考えました。ただ、そういう作りにしてしまうと、特殊なシチュエーションでかゆいところに手が届かないみたいなことになってしまったり、アプリケーションによっては、まったく違うインターフェースの方がマッチしたり、といったことが起こり得ると考えて、まずはナイーブにtokyocabinet.idlに準拠したインターフェースにしました。

Haskellを使っているわりにはなにかと面倒、と感じる部分もでてくると思いますが、tokyocabinet.idlに沿うことによって、TCのメインの機能はHaskellをほとんど知らなくても直感的に使えるようになっていると思います。(なってなかったらごめんなさい。)また、これらを土台にして、より抽象的な関数を作ることはいくらでもできると思います。
実用的かどうかはわかりませんが、ReaderモナドやErrorモナドと組み合わせたサンプルもリポジトリにコミットしています。

まとめ

TCのHaskellバインディングを簡単に紹介させていただきました。PerlやRubyから使えるのだからそれで十分だろうという声もあるかと思いますが、私はHaskellが好きでかつTCを使いたかったのでバインディングを書きました。
Haskellというと、まだまだ実用にはならない言語というイメージを持たれがちで、社内でもHaskellというと、"ネタでしょ"みたいな扱いを受けたりもします。
しかし、最近ではReal World Haskellという本が出版されるなど、実用的な言語としてのHaskellも注目を集めています。日本語のものでもshelarcyさんの連載など有益な情報源が増えてきています。
今回作ったTCのバインディングが、実用的なアプリを作る土台になっていけばいいなと思っています。