{-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-redundant-constraints #-} module Bookkeeper.Internal where import GHC.OverloadedLabels import GHC.Generics import qualified Data.Type.Map as Map import GHC.TypeLits (Symbol, KnownSymbol, TypeError, ErrorMessage(..)) import Data.Default.Class (Default(..)) import Data.Kind (Type) import Data.Type.Map (Map, Mapping((:->))) import Data.Monoid ((<>)) import Data.List (intercalate) import Bookkeeper.Internal.Errors ------------------------------------------------------------------------------ -- Book ------------------------------------------------------------------------------ -- Using a type synonym allows the user to write the fields in any order, and -- yet have the underlying value always have sorted fields. type Book a = Book' (Map.AsMap a) -- | The internal representation of a Book. newtype Book' (a :: [Mapping Symbol Type]) = Book { getBook :: Map a } instance ShowHelper (Book' a) => Show (Book' a) where show x = "Book {" <> intercalate ", " (go <$> showHelper x) <> "}" where go (k, v) = k <> " = " <> v class ShowHelper a where showHelper :: a -> [(String, String)] instance ShowHelper (Book' '[]) where showHelper _ = [] instance ( ShowHelper (Book' xs) , KnownSymbol k , Show v ) => ShowHelper (Book' ((k :=> v) ': xs)) where showHelper (Book (Map.Ext k v rest)) = (show k, show v):showHelper (Book rest) instance Eq (Book' '[]) where _ == _ = True instance (Eq val, Eq (Book' xs)) => Eq (Book' ((field :=> val) ': xs) ) where Book (Map.Ext _ a as) == Book (Map.Ext _ b bs) = a == b && Book as == Book bs instance Monoid (Book' '[]) where mempty = emptyBook _ `mappend` _ = emptyBook instance Default (Book' '[]) where def = emptyBook instance ( Default (Book' xs) , Default v ) => Default (Book' ((k :=> v) ': xs)) where def = Book (Map.Ext Map.Var def (getBook def)) -- | A book with no records. You'll usually want to use this to construct -- books. emptyBook :: Book '[] emptyBook = Book Map.Empty ------------------------------------------------------------------------------ -- Other types ------------------------------------------------------------------------------ -- | An alias for ':->' because otherwise you'll have to tick your -- constructors. type a :=> b = a ':-> b instance (s ~ s') => IsLabel s (Key s') where fromLabel _ = Key -- | 'Key' is simply a proxy. You will usually not need to generate it -- directly, as it is generated by the OverlodadedLabels magic. data Key (a :: Symbol) = Key deriving (Eq, Show, Read, Generic) ------------------------------------------------------------------------------ -- Setters and getters ------------------------------------------------------------------------------ -- * Getters -- | @Gettable field val book@ is the constraint needed to get a value of type -- @val@ from the field @field@ in the book of type @Book book@. type Gettable field book val = (Map.Submap '[field :=> val] book, Contains book field val) -- | Get a value by key, if it exists. -- -- >>> get #age julian -- 28 -- -- If the key does not exist, throws a type error -- >>> get #moneyFrom julian -- ... -- ... • The provided Book does not contain the field "moneyFrom" -- ... Book type: -- ... '["age" ':-> Int, "name" ':-> String] -- ... • In the expression: get #moneyFrom julian -- ... get :: forall field book val. (Gettable field book val) => Key field -> Book' book -> val get _ (Book bk) = case (Map.submap bk :: Map '[field :=> val]) of Map.Ext _ v Map.Empty -> v -- | Flipped and infix version of 'get'. -- -- >>> julian ?: #name -- "Julian K. Arni" (?:) :: forall field book val. (Gettable field book val) => Book' book -> Key field -> val (?:) = flip get infixl 3 ?: -- * Setters -- | 'Settable field val old new' is a constraint needed to set the the field -- 'field' to a value of type 'val' in the book of type 'Book old'. The -- resulting book will have type 'Book new'. type Settable field val old new = ( Map.Submap (Map.AsMap (old Map.:\ field)) old , Map.Unionable '[ field :=> val] (Map.AsMap (old Map.:\ field)) , new ~ Map.AsMap (( field :=> val) ': (Map.AsMap (old Map.:\ field))) ) -- | Sets or updates a field to a value. -- -- >>> set #likesDoctest True julian -- Book {age = 28, likesDoctest = True, name = "Julian K. Arni"} set :: forall field val old new . ( Settable field val old new) => Key field -> val -> Book' old -> Book' new set p v old = Book new where Book deleted = delete p old added = Map.Ext (Map.Var :: Map.Var field) v deleted new = Map.asMap added -- | Infix version of 'set' -- -- >>> julian & #age =: 29 -- Book {age = 29, name = "Julian K. Arni"} (=:) :: ( Settable field val old new) => Key field -> val -> Book' old -> Book' new (=:) = set infix 3 =: -- * Modifiers -- | @Modifiable field val val' old new@ is a constraint needed to apply a -- function of type @val -> val'@ to the field @field@ in the book of type -- @Book old@. The resulting book will have type @Book new@. type Modifiable field val val' old new = ( Settable field val' old new , Map.AsMap new ~ new , Contains old field val , Map.Submap '[ field :=> val] old ) -- | Apply a function to a field. -- -- >>> julian & modify #name (fmap toUpper) -- Book {age = 28, name = "JULIAN K. ARNI"} -- -- If the key does not exist, throws a type error -- >>> modify #height (\_ -> 132) julian -- ... -- ... • The provided Book does not contain the field "height" -- ... Book type: -- ... '["age" ':-> Int, "name" ':-> String] -- ... • In the expression: modify #height (\ _ -> 132) julian -- ... modify :: ( Modifiable field val val' old new) => Key field -> (val -> val') -> Book' old -> Book new modify p f b = set p v b where v = f $ get p b -- | Infix version of 'modify'. -- -- >>> julian & #name %: fmap toUpper -- Book {age = 28, name = "JULIAN K. ARNI"} (%:) :: ( Modifiable field val val' old new) => Key field -> (val -> val') -> Book' old -> Book new (%:) = modify infixr 3 %: -- | Delete a field from a 'Book', if it exists. If it does not, returns the -- @Book@ unmodified. -- -- >>> get #name $ delete #name julian -- ... -- ... • The provided Book does not contain the field "name" -- ... Book type: -- ... '["age" ':-> Int] -- ... • In the expression: get #name -- ... delete :: forall field old . ( Map.Submap (Map.AsMap (old Map.:\ field)) old ) => Key field -> Book' old -> Book (old Map.:\ field) delete _ (Book bk) = Book $ Map.submap bk -- * Generics class FromGeneric a book | a -> book where fromGeneric :: a x -> Book' book instance FromGeneric cs book => FromGeneric (D1 m cs) book where fromGeneric (M1 xs) = fromGeneric xs instance FromGeneric cs book => FromGeneric (C1 m cs) book where fromGeneric (M1 xs) = fromGeneric xs instance (v ~ Map.AsMap ('[name ':-> t])) => FromGeneric (S1 ('MetaSel ('Just name) p s l) (Rec0 t)) v where fromGeneric (M1 (K1 t)) = (Key =: t) emptyBook instance ( FromGeneric l lbook , FromGeneric r rbook , Map.Unionable lbook rbook , book ~ Map.Union lbook rbook ) => FromGeneric (l :*: r) book where fromGeneric (l :*: r) = Book $ Map.union (getBook (fromGeneric l)) (getBook (fromGeneric r)) type family Expected a where Expected (l :+: r) = TypeError ('Text "Cannot convert sum types into Books") Expected U1 = TypeError ('Text "Cannot convert non-record types into Books") instance (book ~ Expected (l :+: r)) => FromGeneric (l :+: r) book where fromGeneric = error "impossible" instance (book ~ Expected U1) => FromGeneric U1 book where fromGeneric = error "impossible" -- | Generate a @Book@ from an ordinary Haskell record via GHC Generics. -- -- >>> data Test = Test { field1 :: String, field2 :: Int, field3 :: Char } deriving Generic -- >>> fromRecord (Test "hello" 0 'c') -- Book {field1 = "hello", field2 = 0, field3 = 'c'} -- -- Trying to convert a datatype which is not a record will result in a type -- error: -- -- >>> data SomeSumType = LeftSide | RightSide deriving Generic -- >>> fromRecord LeftSide -- ... -- ... • Cannot convert sum types into Books -- ... -- -- >>> data Unit = Unit deriving Generic -- >>> fromRecord Unit -- ... -- ... • Cannot convert non-record types into Books -- ... fromRecord :: (Generic a, FromGeneric (Rep a) bookRep) => a -> Book' bookRep fromRecord = fromGeneric . from -- $setup -- >>> import Data.Function ((&)) -- >>> import Data.Char (toUpper) -- >>> type Person = Book '[ "name" :=> String , "age" :=> Int ] -- >>> let julian :: Person = emptyBook & #age =: 28 & #name =: "Julian K. Arni"