デシリアライザとスキーマ

盛大に遅れました…

qiita.com

最近思いついたネタで実用性の高そうなものを紹介。

binarycerealのようなライブラリはデータを密にシリアライズするが、その際にフィールド名や型などの情報は失われてしまう。かといってそれらを一つ一つすべて含めるとひどく効率の悪いフォーマットになってしまう。そこで、スキーマを分離できるような仕組みを作れないかと考えて作ったのがこのクラスだ。

{-# LANGUAGE TypeFamilies, ScopedTypeVariables, FlexibleContexts, UndecidableInstances #-}
import Data.Binary

class HasSchema a where
    type Schema a :: *
    toSerializer :: a -> Put
    toDeserializer :: Schema a -> Get a

HasSchemaクラスは、普通にシリアライズするメソッドtoSerializerと、スキーマを使ってデシリアライズするtoDeserializerを持っている。Schemaは型族なのでどんなものでも使える(一般にどうあるべきかよく考えていない)。

instance HasSchema a => HasSchema [a] where
    type Schema [a] = [Schema a]
    toSerializer = mapM toSerializer
    toDeserializer = mapM toDeserializer

同じスキーマの値が連続している場合、以下のTableという型を使うと無駄がない。

newtype Table a = Table (V.Vector a)

instance HasSchema a => HasSchema (Table a) where
    type Schema (Table a) = Schema a
    toSerializer (Table v) = put (V.length v) >> mapM toSerializer v
    toDeserializer sch = get
        >>= \n -> Table <$> V.fromListN n <$> replicateM n (toDeserializer sch)

このSchematicというデータ型はスキーマとデータの対にすぎないが、これで包んでおくことで簡単にHasSchemaの力を引き出せる。

data Schematic a = Schematic !(Schema a) !a

instance (HasSchema a, Binary (Schema a)) => Binary (Schematic a) where
    get = get >>= \sch -> Schematic sch <$> toDeserializer sch
    put (Schematic sch a) = put sch >> toSerializer a

今のところ、社内で使っているレコード型を簡単にシリアライズするために実験的に使っているが、より一般的で面白い使い方があるかもしれない。この記事では、実体のあるHasSchemaインスタンス(instance HasSchema Intなど)は一切定義されていない。どう作っていくかは、これからの課題だ。