Just to give you some inspiration, just do what Servant does, and you have different types for the different combinators that you support:
{-
Yes, it's pretty Spartan, but it already allows you to do
type MyFormat = Token 4 :>> Lit "PERSON" :>> Skip 1 :>> Token 4 testDeserialize :: String -> Maybe MyFormat testDeserialize = fmap fst . deserialize
which works as follows:
*Seriavant> testDeserialize "1" Nothing *Seriavant> testDeserialize "1234PERSON Foo " Just (Token "1234" :>> (Lit :>> (Skip :>> Token "Foo ")))
EDIT . It turns out that I completely misunderstood the question, and Sean asks for serialization, not deserialization ... But, of course, we can do this:
class Serialize a where serialize :: a -> String instance (KnownNat n) => Serialize (Skip n) where serialize Skip = replicate (fromIntegral $ natVal (Proxy :: Proxy n)) ' ' instance (KnownNat n) => Serialize (Token n) where serialize (Token t) = pad (fromIntegral $ natVal (Proxy :: Proxy n)) ' ' t instance (KnownSymbol lit) => Serialize (Lit lit) where serialize Lit = symbolVal (Proxy :: Proxy lit) instance (Serialize a, Serialize b) => Serialize (a :>> b) where serialize (x :>> y) = serialize x ++ serialize y pad :: Int -> a -> [a] -> [a] pad 0 _x0 xs = xs pad n x0 (x:xs) = x : pad (n-1) x0 xs pad n x0 [] = replicate n x0
(of course, this has terrible performance with all this String
concatenation, etc., but itβs not)
*Seriavant> serialize ((Token "1234" :: Token 4) :>> (Lit :: Lit "FOO") :>> (Skip :: Skip 2) :>> (Token "Bar" :: Token 10)) "1234FOO Bar "
Of course, if we know the format, we can avoid these annoying annotations like:
type MyFormat = Token 4 :>> Lit "PERSON" :>> Skip 1 :>> Token 4 testSerialize :: MyFormat -> String testSerialize = serialize
*Seriavant> testSerialize (Token "1234" :>> Lit :>> Skip :>> Token "Bar") "1234PERSON Bar "