{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, UndecidableInstances #-} {-# LANGUAGE Safe, GADTs, DefaultSignatures, KindSignatures, DataKinds, CPP #-} {-# LANGUAGE PatternSynonyms, ViewPatterns #-} -- For [ghc-8.0 .. ghc-8.4) support. #if __GLASGOW_HASKELL__ < 840 {-# LANGUAGE TypeInType #-} #endif {- | License : BSD-style Module : Data.Property Copyright : (c) Andrey Mulik 2020 Maintainer : work.a.mulik@gmail.com @Data.Property@ new-style properties. -} module Data.Property ( -- * Generalized properties IsProp (..), PropertyKind, FieldKind, Prop (..), -- ** Basic properties FieldGet (..), get, gets', FieldSet (..), set, sets', -- *** Set properties pattern (:=), pattern (::=), pattern (:=$), pattern (::=$), -- *** Monadic set properties pattern (:<=), pattern (:<=$), pattern (:=<), pattern (:=<$), -- ** Modify properties FieldModify (..), -- *** Modify properties pattern (:~), pattern (:~$), pattern (::~), pattern (::~$), -- *** Monadic modify properties pattern (:<~), pattern (:<~$), pattern (:~<), pattern (:~<$), -- ** Switch properties IsSwitch (..), FieldSwitch (..), switch, incr, decr ) where import Data.Typeable import Data.Kind import Control.Monad default () -------------------------------------------------------------------------------- {- Generalized property. -} {- | @since 0.2 'Prop' is new, generalized and extensible property type (existential), which may contain any 'IsProp' value. -} data Prop m field record where Prop :: (Monad m, IsProp prop) => prop m field record -> Prop m field record deriving ( Typeable ) {- | @since 0.2 'IsProp' is a property class that allows you to extend @fmr@ syntax. Now you can create new property types and use it with existing in 'set' list of actions. -} class IsProp (prop :: PropertyKind) where -- | @performProp record prop @ performs an action on @record@ using @prop@. performProp :: (Monad m) => record -> prop m field record -> m () instance IsProp SetProp where performProp record (SetRecordPropM field kl) = setRecords field record =<< kl record performProp record (SetRecordProp field f) = setRecords field record (f record) performProp record (SetPropM field mval) = setRecords field record =<< mval performProp record (SetProp field val) = setRecords field record val setRecords :: (Monad m, FieldSet field) => [field m record a] -> record -> a -> m () setRecords fields record val = forM_ fields $ \ field -> setRecord field record val instance IsProp ModifyProp where performProp record (Modify field f) = () <$ modifyRecords field record (f record) performProp record (ModifyM field f) = () <$ modifyRecordsM field record (f record) performProp record (ModifyProp field f) = () <$ modifyRecords field record f performProp record (ModifyPropM field f) = () <$ modifyRecordsM field record f modifyRecords :: (Monad m, FieldModify field) => [field m record b] -> record -> (b -> b) -> m () modifyRecords fields record f = fields `forM_` \ field -> modifyRecord field record f modifyRecordsM :: (Monad m, FieldModify field, FieldGet field) => [field m record b] -> record -> (b -> m b) -> m () modifyRecordsM fields record f = fields `forM_` \ field -> modifyRecordM field record f instance IsProp SwitchProp where performProp record (SwitchProp n field) = switchRecord field record n -------------------------------------------------------------------------------- -- | @since 0.2 Service kind synonym. type FieldKind = (Type -> Type) -> Type -> Type -> Type -- | @since 0.2 Service kind synonym. type PropertyKind = (Type -> Type) -> FieldKind -> Type -> Type -------------------------------------------------------------------------------- {- fmr classes. -} -- | @since 0.2 Property getter class. class FieldGet field where -- | @'getRecord' field record@ return @record@'s value using @field@. getRecord :: (Monad m) => field m record a -> record -> m a -- | @since 0.2 Property setter class. class FieldSet field where -- | @'setRecord' field record value@ sets new @record@ @value@. setRecord :: (Monad m) => field m record a -> record -> a -> m () {- | @since 0.2 Property modifier class. Note that 'FieldModifier' doesn't go well with write-only fields, because 'modifyRecord' returns new value, and 'modifyRecordM' also assumes the possibility of old value \"leaking\" (hence the 'FieldGet' constraint is imposed on it). -} class (FieldSet field) => FieldModify field where {- | @'modifyRecord' field record upd@ modifies @record@ @field@ using @upd@. Returns new value. -} default modifyRecord :: (Monad m, FieldGet field) => field m record a -> record -> (a -> a) -> m a modifyRecord :: (Monad m) => field m record a -> record -> (a -> a) -> m a modifyRecord field record f = do val <- f <$> get field record setRecord field record val return val {- | @'modifyRecordM' field record upd@ modifies @record@ @field@ using @upd@. Note that assumes the possibility of old value \"leaking\", e.g.: @ -- get value using 'modifyRecordM' getLeak = do -- Some read-only fields x <- newWriteOnly y <- 'var' Nothing -- write current value to y and do not modify. modifyRecordM this x (\ val -> do set [this := Just val]; return val) -- leaking: return current value of "write-only" record get this y @ So you cannot use it for write-only fields. -} modifyRecordM :: (Monad m, FieldGet field) => field m record a -> record -> (a -> m a) -> m a modifyRecordM field record f = do val <- f =<< get field record setRecord field record val return val {- | Switch property modifier. Note that 'FieldSwitch' is designed for deterministic switches with a pre-known set of states and order of transitions between them, without branches. If you need more complex behavior, use 'FieldModify'. -} class FieldSwitch field where -- | Repeated increment or decrement. switchRecord :: (Monad m, IsSwitch a) => field m record a -> record -> Int -> m () -------------------------------------------------------------------------------- {- fmr properties. -} {- | @since 0.2 'SetProp' is a service type used to set field values. See @(':=')@, @(':=$')@, @('::=')@, @('::=$')@, @(':<=')@, @(':<=$')@, @(':=<')@ and @(':=<$')@ patterns. -} data SetProp m field record where -- | 'SetProp' corresponds to @(':=$')@ and @(':=')@. SetProp :: (FieldSet field) => [field m record a] -> a -> SetProp m field record -- | 'SetPropM' corresponds to @('::=$')@ and @('::=')@. SetPropM :: (FieldSet field) => [field m record a] -> m a -> SetProp m field record -- | 'SetRecordProp' corresponds to @(':<=$')@ and @(':<=')@. SetRecordProp :: (FieldSet field) => [field m record a] -> (record -> a) -> SetProp m field record -- | 'SetRecordPropM' corresponds to @(':=<$')@ and @(':=<')@. SetRecordPropM :: (FieldSet field) => [field m record a] -> (record -> m a) -> SetProp m field record deriving ( Typeable ) {- | @since 0.2 'ModifyProp' is a service type used to modify field values. See @(':~')@, @(':~$')@, @(':<~')@, @(':<~$')@, @('::~')@, @('::~$')@, @(':~<')@, @(':~<$')@ patterns. -} data ModifyProp m field record where -- | 'FieldModify' constructor corresponds to @(':~$')@ and @(':~')@. ModifyProp :: (FieldModify field) => [field m record a] -> (a -> a) -> ModifyProp m field record -- | 'FieldModify' constructor corresponds to @(':<~$')@ and @(':<~')@. ModifyPropM :: (FieldModify field, FieldGet field) => [field m record a] -> (a -> m a) -> ModifyProp m field record -- | 'Modify' constructor corresponds to @('::~$')@ and @('::~')@. Modify :: (FieldModify field) => [field m record a] -> (record -> a -> a) -> ModifyProp m field record -- | 'ModifyM' constructor corresponds to @(':~<$')@ and @(':~<')@. ModifyM :: (FieldModify field, FieldGet field) => [field m record a] -> (record -> a -> m a) -> ModifyProp m field record deriving ( Typeable ) {- | @since 0.2 'SwitchProp' is a service type used to update record values, see 'switch', 'incr' and 'decr'. -} data SwitchProp m field record where -- | Switch field, see 'switch', 'incr' and 'decr'. SwitchProp :: (Monad m, FieldSwitch field, IsSwitch a) => Int -> field m record a -> SwitchProp m field record deriving ( Typeable ) -------------------------------------------------------------------------------- {- fmr pure setters. -} {- | Pure value setter. @set record [field := value]@ set @value@ to @record@'s @field@. -} pattern (:=) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldSet field ) => field m record a -> a -> Prop m field record pattern field := val = [field] :=$ val {- | Pure value setter with @record@. @set record [field ::= f]@ set @f record@ to @record@'s @field@. @ set record [field ::= const val] === set record [field := val] @ -} pattern (::=) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldSet field ) => field m record a -> (record -> a) -> Prop m field record pattern field ::= f = [field] ::=$ f {- | @since 0.2 Pure group setter. @set record [fields :=$ value]@ set @value@ to @record@'s some @fields@. @ set record [[field] :=$ value] === set record [field := value] @ -} pattern (:=$) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldSet field ) => [field m record a] -> a -> Prop m field record pattern field :=$ val <- (cast -> Just (SetProp field val)) where (:=$) = Prop ... SetProp {- | @since 0.2 Pure group setter with @record@. @set record [fields ::=$ f]@ set @f record@ to @record@'s some @fields@. @ set record [[field] ::=$ f] === set record [field ::= f] set record [fields ::=$ const val] === set record [fields :=$ val] @ -} pattern (::=$) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldSet field ) => [field m record a] -> (record -> a) -> Prop m field record pattern field ::=$ f <- (cast -> Just (SetRecordProp field f)) where (::=$) = Prop ... SetRecordProp -------------------------------------------------------------------------------- {- fmr pure updaters. -} {- | Pure value modifier. @set record [field :~ f]@ modify value of @record@'s @field@ using @f@ function. @ set record [field :~ const val] === set record [field := val] @ -} pattern (:~) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldModify field ) => field m record a -> (a -> a) -> Prop m field record pattern field :~ f = [field] :~$ f {- | Pure value modifier with @record@. @set record [field ::~ f]@ modify value of @record@'s @field@ using @f record@ function. @ set record [field ::~ const f] === set record [field :~ f] @ -} pattern (::~) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldModify field ) => field m record a -> (record -> a -> a) -> Prop m field record pattern field ::~ f = [field] ::~$ f {- | @since 0.2 Pure group modifier. @set record [fields :~$ f]@ modify values of @record@'s @fields@ using @f@ function. @ set record [[field] :~$ val] === set record [field :~ val] set record [fields :~$ const val] === set record [field :=$ val] @ -} pattern (:~$) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldModify field ) => [field m record a] -> (a -> a) -> Prop m field record pattern field :~$ f <- (cast -> Just (ModifyProp field f)) where (:~$) = Prop ... ModifyProp {- | @since 0.2 Pure group modifier with @record@. @set record [fields ::~$ f]@ modify values of @record@'s @fields@ using @f record@ function. @ set record [[field] ::~$ f] === set record [field ::~ f] set record [fields ::~$ const f] === set record [field :~$ f] @ -} pattern (::~$) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldModify field ) => [field m record a] -> (record -> a -> a) -> Prop m field record pattern field ::~$ f <- (cast -> Just (Modify field f)) where (::~$) = Prop ... Modify -------------------------------------------------------------------------------- {- fmr monadic setters. -} {- | @since 0.2 Monadic value setter. @set record [field :<= mvalue]@ set result of @mvalue@ to @record@'s @field@. Note that the @mvalue@ is evaluated every time a @field@ value is assigned. @ set record [field :<= return val] === set record [field := val] set record [field :<= mval] === do val <- mval; set record [field :<= val] @ -} pattern (:<=) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldSet field ) => field m record a -> m a -> Prop m field record pattern field :<= mval = [field] :<=$ mval {- | @since 0.2 Monadic value setter with @record@. @set record [field :=< mvalue]@ set result of @mvalue record@ to @record@'s @field@. Note that the @mvalue@ is evaluated every time a @field@ value is assigned. @ set record [field :=< const val] === set record [field :<= val] set record [field :=< f] === do val <- f record; set record [field := val] @ -} pattern (:=<) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldSet field ) => field m record a -> (record -> m a) -> Prop m field record pattern field :=< f = [field] :=<$ f {- | @since 0.2 Monadic group setter. @set record [fields :<=$ mvalue]@ set result of @mvalue@ to @record@'s @fields@. Note that @mvalue@ is evaluated only once, on the first assignment. Thus, the values of all the listed fields will be identical. @ set record [[field] :<=$ const f] === set record [field :<= val] set record [fields :<=$ mval] === do val <- mval; set record [fields :<=$ val] @ -} pattern (:<=$) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldSet field ) => [field m record a] -> m a -> Prop m field record pattern field :<=$ mval <- (cast -> Just (SetPropM field mval)) where (:<=$) = Prop ... SetPropM {- | @since 0.2 Monadic group setter with @record@. @set record [fields :=<$ f]@ set result of @f record@ to @record@'s @fields@. Note that @f record@ is evaluated only once, on the first assignment. Thus, the values of all the listed fields will be identical. @ set record [[field] :=<$ f] === set record [field :=< val] set record [fields :=<$ const val] = set record [fields :=$ val] set record [fields :=<$ f] === do val <- f record; set record [fields :=$ val] @ -} pattern (:=<$) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldSet field ) => [field m record a] -> (record -> m a) -> Prop m field record pattern field :=<$ f <- (cast -> Just (SetRecordPropM field f)) where (:=<$) = Prop ... SetRecordPropM -------------------------------------------------------------------------------- {- fmr monadic updaters. -} {- | @since 0.2 Monadic value modifier. @set record [field :<~ f]@ modifies value of @record@'s @field@ using @f@ procedure. Note that the @mvalue@ is called every time a @field@ value is assigned. @ set record [field :<~ return val] === set record [fields := val] @ -} pattern (:<~) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldModify field, FieldGet field ) => field m record a -> (a -> m a) -> Prop m field record pattern field :<~ f = [field] :<~$ f {- | @since 0.2 Monadic value modifier with @record@. @set record [field :<~ f]@ modifies value of @record@'s @field@ using @f record@ procedure. Note that the @f record@ is called every time a @field@ value is assigned. @ set record [field :~< const f] === set record [fields :<~ f] @ -} pattern (:~<) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldModify field, FieldGet field ) => field m record a -> (record -> a -> m a) -> Prop m field record pattern field :~< f = [field] :~<$ f {- | @since 0.2 Monadic group modifier. @set record [fields :<~$ f]@ modifies values of @record@'s @fields@ using @f@ procedure. @ set record [[field] :<~$ f] === set record [field :<~ f] set record [fields :<~$ const mval] === set record [field :<=$ mval] @ -} pattern (:<~$) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldModify field, FieldGet field ) => [field m record a] -> (a -> m a) -> Prop m field record pattern field :<~$ f <- (cast -> Just (ModifyPropM field f)) where (:<~$) = Prop ... ModifyPropM {- | @since 0.2 Monadic group modifier with @record@. @set record [fields :~<$ f]@ modifies values of @record@'s @fields@ using @f record@ procedure. Note that the @f record@ is called every time a @field@ value is assigned. @ set record [field :~<$ const f] === set record [fields :<~$ f] @ -} pattern (:~<$) :: ( Typeable m, Typeable field, Typeable record, Monad m, FieldModify field, FieldGet field ) => [field m record a] -> (record -> a -> m a) -> Prop m field record pattern field :~<$ f <- (cast -> Just (ModifyM field f)) where (:~<$) = Prop ... ModifyM -------------------------------------------------------------------------------- -- | Service class for switchable types. class IsSwitch switch where {- | @'toggle' s n@ "toggles" the state represented by the value @s@ by @n@ positions, for example: @ toggle n False = even n toggle n True = odd n toggle n 1234 = 1239 + n @ -} toggle :: Int -> switch -> switch instance IsSwitch Bool where toggle n False = even n toggle n True = odd n instance (Integral i) => IsSwitch i where toggle n i = fromIntegral n + i -------------------------------------------------------------------------------- -- | The 'get' function reads current value of a field. get :: (Monad m, FieldGet field) => field m record a -> record -> m a get = getRecord -- | @'gets'' fields record@ returns list of @record@ @fields@ values. gets' :: (Monad m, FieldGet field) => record -> [field m record a] -> m [a] gets' = mapM . flip getRecord {- | 'set' is the main function in @fmr@, which allows you to describe changing the value of a record as a sequence of operations on its fields, e.g. -} set :: (Monad m) => record -> [Prop m field record] -> m () set record = mapM_ $ \ (Prop prop) -> performProp record prop -- | Just synonym for 'set'. sets' :: (Monad m) => record -> [Prop m field record] -> m () sets' = set -- | 'switch' changes the value by n steps. switch :: (Monad m, FieldSwitch field, IsSwitch a) => field m record a -> Int -> Prop m field record switch field n = Prop (SwitchProp n field) -- | @'incr' field@ is same as @switch field 1@. incr :: (Monad m, FieldSwitch field, IsSwitch a) => field m record a -> Prop m field record incr = Prop . SwitchProp 1 -- | @'decr' field@ is same as @switch field (-1)@. decr :: (Monad m, FieldSwitch field, IsSwitch a) => field m record a -> Prop m field record decr = Prop . SwitchProp (-1) -------------------------------------------------------------------------------- -- | @sdp@ @(.)@-like combinator. (...) :: (c -> d) -> (a -> b -> c) -> a -> b -> d (...) = (.) . (.)