-- | This module provides some tools to encode multiple versions -- of a data model in a single data-type parametrized by version number. -- The addition or removal of a field can be expressed through the -- 'Since' and 'Until' type families. -- -- Example: -- -- > data Rec v = Rec -- > { foo :: Int -- this field exists in all versions -- > , bar :: Since V2 v Bool -- this field has been introduced in V2 -- > , baz :: Until V2 v Double -- this field has been removed in V3 -- > } -- -- Besides reducing the number of data declarations, -- this approach also has other advantages: -- -- * It makes migrations declarative and self-documenting. -- -- * It allows for less verbose version-upcasting functions, -- since the fields that have a non-parametric type do not need to be copied. -- -- * It is a foundation on which other useful abstractions can be built. -- -- Please note that some classes may require a separate standalone deriving clause -- for each version of a data-type or some kind of inductive deriving mechanism. {-# LANGUAGE DataKinds #-} {-# LANGUAGE ExplicitNamespaces #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} module Versioning.Base ( -- * Types V (..) , VPred , VSucc , VNat , VCmp , Since , SinceS , Until , UntilS , NA , na , V0 , V1 , V2 , V3 , V4 , V5 , V6 , V7 , V8 , V9 , V10 , V11 , V12 , V13 , V14 , V15 , V16 , V17 , V18 , V19 , V20 -- * Functions , versionNumber ) where import Data.Proxy (Proxy (..)) import GHC.TypeNats (type (+), KnownNat, Nat, natVal) import Numeric.Natural (Natural) import Versioning.Internal.Base (Bare) -- | The version of a data model data V = VZero | VSucc V deriving (Eq, Show) -- | Get the previous version type family VPred (v :: V) :: V where VPred ('VSucc v) = v VPred 'VZero = 'VZero type VSucc = 'VSucc -- | Get the type-level natural of a version type family VNat (v :: V) :: Nat where VNat v = VNat' v 0 type family VNat' (v :: V) (n :: Nat) :: Nat where VNat' 'VZero n = n VNat' ('VSucc v) n = VNat' v (n + 1) -- | Compare two versions type family VCmp (v :: V) (w :: V) :: Ordering where VCmp 'VZero 'VZero = 'EQ VCmp 'VZero v = 'LT VCmp v 'VZero = 'GT VCmp ('VSucc v) ('VSucc w) = VCmp v w -- | Get the version number of a versioned value versionNumber :: forall a v. KnownNat (VNat v) => a v -> Natural versionNumber _ = natVal (Proxy :: Proxy (VNat v)) -- | This allows us to express that a field is only present since -- a given version. -- The first parameter is the version in which the field has been introduced, -- the second parameter is the actual version of the data-type. type family Since (s :: V) (v :: V) a where Since s v a = Since' (VCmp s v) a NA -- | Same as 'Since', for sum types. -- The only difference between 'Since' and 'SinceS' is in the type used to -- indicate absence. -- In 'Since' absence is expressed with 'NA', which is isomorphic to '()'. -- In 'SinceS' it is expressed with 'Bare', which is isomorphic to 'Void'. type family SinceS (s :: V) (v :: V) a where SinceS s v a = Since' (VCmp s v) a Bare type family Since' (o :: Ordering) a absent :: * where Since' 'LT a _ = a Since' 'EQ a _ = a Since' 'GT a absent = absent -- | This allows us to express that a field is only present until -- a given version. -- The first parameter is the last version in which the field is present, -- the second parameter is the actual version of the data-type. type family Until (u :: V) (v :: V) a where Until u v a = Until' (VCmp u v) a NA -- | Same as 'Until', for sum types. type family UntilS (u :: V) (v :: V) a where UntilS u v a = Until' (VCmp u v) a Bare type family Until' (o :: Ordering) a absent :: * where Until' 'GT a _ = a Until' 'EQ a _ = a Until' 'LT a absent = absent -- | A type indicating absence. -- The 'Maybe' is a hack needed to let aeson parse a record successfully even -- if a field of type 'NA' is missing. -- -- Ideally we would like to define it as -- -- > data NA = NA -- -- but this would not work with 'FromJSON' instances that are derived -- with Generic. type NA = Maybe Bare -- | A placeholder for an absent value. na :: NA na = Nothing -- Version type synonyms type V0 = 'VZero type V1 = 'VSucc V0 type V2 = 'VSucc V1 type V3 = 'VSucc V2 type V4 = 'VSucc V3 type V5 = 'VSucc V4 type V6 = 'VSucc V5 type V7 = 'VSucc V6 type V8 = 'VSucc V7 type V9 = 'VSucc V8 type V10 = 'VSucc V9 type V11 = 'VSucc V10 type V12 = 'VSucc V11 type V13 = 'VSucc V12 type V14 = 'VSucc V13 type V15 = 'VSucc V14 type V16 = 'VSucc V15 type V17 = 'VSucc V16 type V18 = 'VSucc V17 type V19 = 'VSucc V18 type V20 = 'VSucc V19