-- | Generic functions to convert plain Haskell records -- to their @vinyl@ representation and back. {-# LANGUAGE DataKinds #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} module Data.Vinyl.Generics.Transform( toVinyl , fromVinyl ) where import Data.Vinyl import Generics.SOP import Generics.SOP.NP import qualified Generics.SOP.Record as SR import GHC.TypeLits -- | This typeclass provides a method to change the -- interpretation functor of a particular @vinyl@ record -- from @P@ to @ElField@. class NatTrans rs where rmap' :: Rec SR.P rs -> Rec ElField rs instance NatTrans '[] where rmap' RNil = RNil instance (NatTrans xs, x ~ '(s, t), KnownSymbol s) => NatTrans (x ': xs) where rmap' ((SR.P x) :& xs) = (Field x) :& rmap' xs -- | This typeclass constrains that given a type-level list of field-name, -- field-type tuples (i.e. @rs@), it is possible to "fold" in general sense -- (i.e. a "catamorphism") over a @vinyl@ record parameterised by @rs@, -- swap all it's constructors with those from @NP@ (N-ary product from -- @generics-sop@) and discard the field names by using the @I@ -- interpretation functor from @generics-sop@ and yield something -- of type @NP I ys@. class Cata rs ys | rs -> ys where recToNP :: Rec ElField rs -> NP I ys instance Cata '[] '[] where recToNP RNil = Nil instance (Cata xs ys, y ~ SR.Snd x) => Cata (x ': xs) (y ': ys) where recToNP ((Field x) :& xs) = (I x) :* recToNP xs -- | Given a plain record, returns the @vinyl@ equivalent with the -- field names as type-level strings, tupled with the field type. -- -- Example: -- -- @ -- import qualified Generics.SOP as S -- import qualified GHC.Generics as G -- -- data MyPlainRecord = MPR { -- age :: Int, -- iscool :: Bool, -- yearbook :: Text -- } deriving (Show, G.Generic) -- -- instance S.Generic MyPlainRecord -- instance S.HasDatatypeInfo MyPlainRecord -- -- Note: requires all 3 instances: G.Generic, S.Generic and S.HasDatatypeInfo -- -- -- this now works! Type signature is optional here. -- convertToVinyl :: MyPlainRecord -- -> Rec ElField '[("age" ::: Int), ("iscool" ::: Bool), ("yearbook" ::: Text)] -- convertToVinyl = toVinyl -- @ -- toVinyl :: (SR.IsRecord a rs, NatTrans rs) => a -> Rec ElField rs toVinyl r = rmap' (cata_NP RNil (:&) np) where np = SR.toRecord r -- | Given a @vinyl@ record, returns the plain record equivalent. -- Requires the equivalent plain record data declaration to be available -- in current scope. -- -- Additionally, it requires explicit type annotation (either using a -- type signature or using @TypeApplications@). -- -- Example: -- -- @ -- import qualified Generics.SOP as S -- import qualified GHC.Generics as G -- -- r1 :: Rec ElField '[("age" ::: Int), ("iscool" ::: Bool), ("yearbook" ::: Text)] -- r1 = xrec (23, True, "!123!") -- -- data MyPlainRecord = MPR { -- age :: Int, -- iscool :: Bool, -- yearbook :: Text -- } deriving (Show, G.Generic) -- -- instance S.Generic MyPlainRecord -- -- Note: Here we need only G.Generic and S.Generic -- -- -- Using explicit type signature -- r2 :: MyPlainRecord -- r2 = fromVinyl r1 -- -- -- or using TypeApplications -- r2' = fromVinyl @MyPlainRecord r1 -- @ -- fromVinyl :: (Generic a, Code a ~ ts, ts ~ '[ys], Cata rs ys) => Rec ElField rs -> a fromVinyl = to . SOP . Z . recToNP