haskell-jp / questions #102 at 2022-09-05 10:11:25 +0900

いくつかのデータ型があらかじめ定義されており、それらはTLV(type length value)で統一的に符号化されるとします。
これに対する符号器/復号器の基本ライブラリを作りますが、データ型は将来追加されるかもしれないので、サードパーティライブラリで拡張可能としたいです。

まず、符号化のために以下のようなクラスを定義します。

class Typeable a => T a where
  typeT  :: Int
  encode :: T -> ByteString
  decode :: ByteString -> T
  fromTLV :: TLV -> Maybe a  -- TLVは後述
  fromTLV (TLV x) = cast x
  toTLS :: a -> TLV
  toTLS = TLV

既存のデータを表す型を A と B とすると、

data A = A ...
data B = B ...

instance A where
  typeT = 0
  encode = ...
  decode = ...

instance B where
  typeT = 1
  encode = ...
  decode = ...

これらの型を同じように扱えるように、 ExistentialQuantification を用いて、以下の型を定義します。

data TLV = forall a. T a => TLV a

符号器で encode を使うのは、 a が与えられるので簡単です。

encodeTLV :: TLV -> ByteString
encodeTLV (TLV x) = encode x

問題は復号器です。TLVのTとLの部分は ByteString に符号化された整数なので、それをパースして V の部分の ByteString を切り出した後に、適切な decode を呼ぶだしたいのですが、どうすれば呼べるでしょうか?

ぱっと思いつくのは、 Int -> ByteString の辞書を用意して、それを引くことです。拡張可能にするのは、この辞書を公開し、利用時に要素を追加可能にする必要がありそうです。

基本的に reflaction に関係する問題だと思います。サードバーティが data C を定義したら、自動的にその decode も呼ばれるような魔法はあるのでしょうか?
正しく理解できているか自信がないんですが
decode の型って
decode :: ByteString -> Maybe a

ではないんでしょうか?だとすると、`read` のように結果の型が定まる限りは自動で Cdecode が呼ばれるでしょうし問題ないのでは、という気がするのですがいかがでしょうか?
型がコンパイル時に定まらなくて、実行時に決まります。入力されたByteStringがTLVを表現していますので、そのTをパースした時点で決まります。
となると、型クラスだけでは不可能で、グローバルな IORef (Map T L) を作って、インスタンスを定義したら都度そこに登録する、みたいな方法しか思いつかないですね... :thinking_face:

余談ですが最初聞いたとき https://haskell.jp/blog/posts/2017/typesafe-precure2.html とよく似てるな、とは思いましたがこちらは集める対象となるモジュールのディレクトリーが予め分かっている場合の話なので、ちょっと対応できなさそうです :disappointed: