KVS から複数件まとめて取得してデータ構築する

はじめに

KVS に限った話ではなく DB などでも同じようなことがありますが データ取得件数が多いときに一件づつ取得していると遅くなってしまう場合があります。

こういうときのために複数件まとめて取得する API が用意されている場合が多いと思います。 Redis には mget という API がありました。

この複数件取得する API とデータ構築などの処理を組み合わせるにはどうするのがいいかなと検討したという内容です。

https://github.com/bigsleep/redis-experiment1

今回使ったコードはここにあります。

やり方

要件としては下の二点になると思います。

  • 取得に使うキーを集める
  • KVS から取得したデータを変換して結果の型に変換する関数を合成する

最初は何かのモナドを使っていい感じに書けないかなと考えていました。

do
    name <- kvsGet "name0" id
    age <- kvsGet "age0" convertInt
    point <- kvsGet "point0" convertInt
    return (UserInfo name age point)

しかしモナドだと取得のキーにもモナド値が使われてしまう場合があり得ます。 例えば下のような感じで書かれてしまうとキーをまとめて取得するということができなくなります。

do
    name <- kvsGet "name0" id
    key <- kvsGet "key0" id
    age <- kvsGet key convertInt
    point <- kvsGet "point0" convertInt
    return (UserInfo name age point)

ということでこういうケースはモナドでなくアプリカティブを使うのがいいのではないかと考えました。

Control.Applicative

Haskell/Applicative functors - Wikibooks, open books for an open world

アプリカティブは >>= 関数が使えないのでモナド値がキーとして使われるのは制限されます。

アプリカティブのインスタンスを定義するあたりまでのコードは下のような感じになりました。

アプリカティブ則などをみたしているか不安なので Free Applicative などを使ってもよかったかもしれません。

type KvsConvert a = StateT [Maybe ByteString] Maybe a

data KvsGets a = KvsGets [ByteString] (KvsConvert a)

instance Functor KvsGets where
    fmap f (KvsGets keys convert) = KvsGets keys (fmap f convert)

instance Applicative KvsGets where
    pure a = KvsGets [] (return a)
    KvsGets keys0 f <*> KvsGets keys1 a = KvsGets (keys0 ++ keys1) (f <*> a)

kvsGet :: ByteString -> (Maybe ByteString -> Maybe a) -> KvsGets a
kvsGet key convert = KvsGets [key] m
    where
    m = do
        vals <- State.get
        (h, tail) <- lift . uncons $ vals
        State.put tail
        lift . convert $ h

hmgetAll :: KvsGets a -> Redis.Redis (Maybe a)
hmgetAll (KvsGets keys m) = do
    vals <- Redis.mget keys
    either (const $ return Nothing) (return . State.evalStateT m) vals

Docker Compose で実行してみる

前の記事にも書きましたが Docker で haskell コンパイル環境を整えて使ってみています。

アプリケーション用のコンテナ以外に KVS やデータベースのコンテナを利用する場合には Docker Compose を使うと簡単にできるようだったので使ってみました。

Docker Compose | Docker Documentation

Docker Hub に Reids, Memcached, MySQL, PostgreSQL など Webアプリで使うものは大体揃っていて これらと連携するようなライブラリを書く時も便利そうでした。

https://hub.docker.com/_/redis/

https://hub.docker.com/_/memcached/

https://hub.docker.com/_/mysql

https://hub.docker.com/_/postgresql/

Docker Compose をインストールして docker-compose.yml を書いて docker-compose run コマンドを実行するという感じで使えました。

docker-compose.yml は下のようになりました。

version: "2"
services:
  redis:
    image: redis:4.0.9
    ports:
      - "6379:6379"
  app:
    image: tkaaad97/haskell-docker:8.2.2
    command: stack exec redis-experiment1-exe
    working_dir: /app
    volumes:
      - .:/app
      - .stack:/root/.stack
    depends_on:
      - redis

ビルド時のコマンドは

docker-compose run --rm app stack build

実行時のコマンドは

docker-compose run --rm app stack exec redis-experiment1-exe

コードの実行結果

実験用のコードは下の感じです。

data SomeData = SomeData Int String String [Int] deriving (Show)

main :: IO ()
main = do
    let entries = [("a", "999"), ("b", "hello"), ("c", "world"), ("d", "[1,2,3]")]
        getList = sequenceA $ map (flip kvsGet id) ["a", "b", "c", "d"] :: KvsGets [ByteString]
        getSomeData = SomeData
            <$> kvsGet "a" (fmap $ read . UTF8.decode . BS.unpack)
            <*> kvsGet "b" (fmap $ UTF8.decode . BS.unpack)
            <*> kvsGet "c" (fmap $ UTF8.decode . BS.unpack)
            <*> kvsGet "d" (fmap $ read . UTF8.decode . BS.unpack)
        connectionInfo = Redis.defaultConnectInfo { Redis.connectHost = "redis" }
    connection <- Redis.checkedConnect connectionInfo
    Redis.runRedis connection $ do
        Redis.mset entries
        a <- hmgetAll getList
        liftIO . print $ a
        b <- hmgetAll getSomeData
        liftIO . print $ b
        c <- hmgetAll $ (,) <$> getList <*> getSomeData
        liftIO . print $ c

期待した通り結果が取得できました。

Just ["999","hello","world","[1,2,3]"]
Just (SomeData 999 "hello" "world" [1,2,3])
Just (["999","hello","world","[1,2,3]"],SomeData 999 "hello" "world" [1,2,3])

まとめ

KVS から複数件まとめて取得してデータ構築する方法について検討してみました。 モナドは使えないけど、アプリカティブを使うことで合成しやすい感じで書けそうなことがわかりました。