fclabels 0.3.0 → 0.4.0
raw patch · 3 files changed
+212/−89 lines, 3 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Data.Record.Label: (%%) :: (Functor f) => a :-> b -> g :-> f a -> g :-> f b
- Data.Record.Label: (%) :: (g :-> a) -> (f :-> g) -> (f :-> a)
- Data.Record.Label: class Lens f
- Data.Record.Label: idL :: a :-> a
- Data.Record.Label: instance Lens ((:->) f)
- Data.Record.Label: lget :: :-> a b -> Getter a b
- Data.Record.Label: list :: Int -> [a] :-> a
- Data.Record.Label: lmap :: (Lens f) => (a -> b, b -> a) -> f a -> f b
- Data.Record.Label: lmod :: :-> a b -> Modifier a b
- Data.Record.Label: lset :: :-> a b -> Setter a b
- Data.Record.Label: maybeNull :: [a] :-> Maybe [a]
- Data.Record.Label: mkLabel :: Getter a b -> Setter a b -> a :-> b
- Data.Record.Label: mkModifier :: Getter a b -> Setter a b -> Modifier a b
+ Data.Record.Label: (<->) :: (a -> b) -> (b -> a) -> a :<->: b
+ Data.Record.Label: Lens :: (a -> b) -> (b -> a) -> :<->: a b
+ Data.Record.Label: bw :: :<->: a b -> b -> a
+ Data.Record.Label: class Iso f
+ Data.Record.Label: data Point f i o
+ Data.Record.Label: for :: (i -> o) -> (f :-> o) -> Point f i o
+ Data.Record.Label: fw :: :<->: a b -> a -> b
+ Data.Record.Label: get :: (f :-> a) -> f -> a
+ Data.Record.Label: instance Applicative (Point f i)
+ Data.Record.Label: instance Category :<->:
+ Data.Record.Label: instance Functor (Point f i)
+ Data.Record.Label: instance Iso ((:->) i)
+ Data.Record.Label: iso :: (Iso f) => a :<->: b -> f a -> f b
+ Data.Record.Label: label :: Getter f a -> Setter f a -> f :-> a
+ Data.Record.Label: mod :: (f :-> a) -> (a -> a) -> f -> f
+ Data.Record.Label: newtype (:->) f a
+ Data.Record.Label: osi :: (Iso f) => a :<->: b -> f b -> f a
+ Data.Record.Label: set :: (f :-> a) -> a -> f -> f
- Data.Record.Label: Label :: Getter a b -> Setter a b -> Modifier a b -> :-> a b
+ Data.Record.Label: Label :: Point f a a -> :-> f a
- Data.Record.Label: data (:->) a b
+ Data.Record.Label: data (:<->:) a b
- Data.Record.Label: type Getter a b = a -> b
+ Data.Record.Label: type Getter f o = f -> o
- Data.Record.Label: type Modifier a b = (b -> b) -> a -> a
+ Data.Record.Label: type Modifier f i o = (o -> i) -> f -> f
- Data.Record.Label: type Setter a b = b -> a -> a
+ Data.Record.Label: type Setter f i = i -> f -> f
Files
- Data/Record/Label.hs +84/−63
- Data/Record/Label/TH.hs +5/−22
- fclabels.cabal +123/−4
Data/Record/Label.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE TypeOperators, TypeSynonymInstances, TemplateHaskell #-} module Data.Record.Label ( -- * Getter, setter and modifier types.@@ -7,93 +7,124 @@ , Modifier -- * Label type.- , (:->) (..)- , mkModifier- , mkLabel-- -- * Identity and composition.- , idL- , (%)+ , Point+ , (:->) (Label)+ , label+ , get, set, mod -- * Bidirectional functor.-- , Lens (..)- , (%%)+ , (:<->:) (..)+ , (<->)+ , Iso (..)+ , for -- * State monadic label operations. , getM, setM, modM, (=:) - -- * Useful example labels.- , list- , maybeNull- -- * Derive labels using Template Haskell. , module Data.Record.Label.TH-- ) where+ )+where -import Prelude hiding ((.), id)+import Prelude hiding ((.), id, mod)+import Control.Applicative import Control.Category-import Control.Monad.State+import Control.Monad.State hiding (get) import Data.Record.Label.TH -type Getter a b = a -> b-type Setter a b = b -> a -> a-type Modifier a b = (b -> b) -> a -> a+type Getter f o = f -> o+type Setter f i = i -> f -> f+type Modifier f i o = (o -> i) -> f -> f -data a :-> b = Label- { lget :: Getter a b- , lset :: Setter a b- , lmod :: Modifier a b+data Point f i o = Point+ { _get :: Getter f o+ , _set :: Setter f i } --- | Create a modifier function out of a getter and a setter.+_mod :: Point f i o -> (o -> i) -> f -> f+_mod l f a = _set l (f (_get l a)) a -mkModifier :: Getter a b -> Setter a b -> Modifier a b-mkModifier gg ss f a = ss (f (gg a)) a+newtype (f :-> a) = Label { unLabel :: Point f a a } --- | Smart constructor for `Label's, the modifier will be computed based on--- getter and setter.+-- Create a label out of a getter and setter. -mkLabel :: Getter a b -> Setter a b -> a :-> b-mkLabel g s = Label g s (mkModifier g s)+label :: Getter f a -> Setter f a -> f :-> a+label g s = Label (Point g s) -idL :: a :-> a-idL = mkLabel id const+-- | Get the getter function from a label. -infixr 8 %-(%) :: (g :-> a) -> (f :-> g) -> (f :-> a)-a % b = Label (lget a . lget b) (lmod b . lset a) (lmod b . lmod a)+get :: (f :-> a) -> f -> a+get = _get . unLabel +-- | Get the setter function from a label.++set :: (f :-> a) -> a -> f -> f+set = _set . unLabel++-- | Get the modifier function from a label.++mod :: (f :-> a) -> (a -> a) -> f -> f+mod = _mod . unLabel+ instance Category (:->) where- id = idL- (.) = (%)+ id = Label (Point id const)+ (Label a) . (Label b) = Label (Point (_get a . _get b) (_mod b . _set a)) --- Apply custom `parser' and 'printer' function. This can be seen as a--- bidirectional functorial map.+instance Functor (Point f i) where+ fmap f x = Point (f . _get x) (_set x) -class Lens f where- lmap :: (a -> b, b -> a) -> f a -> f b+instance Applicative (Point f i) where+ pure a = Point (const a) (const id)+ a <*> b = Point (\f -> _get a f (_get b f)) (\r -> _set b r . _set a r) -instance Lens ((:->) f) where- lmap (f, g) (Label a b c) = Label (f . a) (b . g) (c . (g.) . (.f))+-- | This isomorphism type class is like a `Functor' but works in two directions. --- | Apply label to lifted value and join afterwards.+class Iso f where+ iso :: a :<->: b -> f a -> f b+ iso (Lens a b) = osi (b <-> a)+ osi :: a :<->: b -> f b -> f a+ osi (Lens a b) = iso (b <-> a) -infixr 8 %%-(%%) :: Functor f => a :-> b -> g :-> f a -> g :-> f b-(%%) a b = let (Label g s _) = a in (fmap g, fmap (\k -> s k (error "unused"))) `lmap` b+-- | The lens datatype, a function that works in two directions. To bad there+-- is no convenient way to do application for this. +data a :<->: b = Lens { fw :: a -> b, bw :: b -> a }++-- | Constructor for lenses.++infixr 7 <->+(<->) :: (a -> b) -> (b -> a) -> a :<->: b+a <-> b = Lens a b++instance Category (:<->:) where+ id = Lens id id+ (Lens a b) . (Lens c d) = Lens (a . c) (d . b)++instance Iso ((:->) i) where+ iso l (Label a) = Label (Point (fw l . _get a) (_set a . bw l))++dimap :: (o' -> o) -> (i -> i') -> Point f i' o' -> Point f i o+dimap f g l = Point (f . _get l) (_set l . g)++-- | Combine a partial destructor with a label into something easily used in+-- the applicative instance for the hidden `Point' datatype. Internally uses+-- the covariant in getter, contravariant in setter bi-functioral-map function.+-- (Please refer to the example because this function is just not explainable+-- on its own.)++for :: (i -> o) -> (f :-> o) -> Point f i o+for a b = dimap id a (unLabel b)+ -- | Get a value out of state pointed to by the specified label. getM :: MonadState s m => s :-> b -> m b-getM = gets . lget+getM = gets . get -- | Set a value somewhere in state pointed to by the specified label. setM :: MonadState s m => s :-> b -> b -> m ()-setM l = modify . lset l+setM l = modify . set l -- | Alias for `setM' that reads like an assignment. @@ -105,15 +136,5 @@ -- specified label. modM :: MonadState s m => s :-> b -> (b -> b) -> m ()-modM l = modify . lmod l---- Lift list indexing to a label.--list :: Int -> [a] :-> a-list i = mkLabel (!! i) (\v a -> take i a ++ [v] ++ drop (i+1) a)---- View null lists as Nothing.--maybeNull :: [a] :-> Maybe [a]-maybeNull = (\x -> if null x then Nothing else Just x, maybe [] id) `lmap` id+modM l = modify . mod l
Data/Record/Label/TH.hs view
@@ -1,22 +1,10 @@ module Data.Record.Label.TH (mkLabels) where -import Control.Monad (liftM)-import Data.Char (toLower, toUpper)-import Language.Haskell.TH- ( Body (NormalB)- , Clause (Clause)- , Con (RecC)- , Dec (DataD, FunD)- , Exp (AppE, LamE, RecUpdE, VarE)- , Info (TyConI)- , Name- , Pat (VarP)- , Q- , mkName- , nameBase- , reify)-import Language.Haskell.TH.Syntax (VarStrictType)+import Control.Monad+import Data.Char+import Language.Haskell.TH.Syntax +-- | Derive labels for all the record selector in a datatype. mkLabels :: [Name] -> Q [Dec] mkLabels = liftM concat . mapM mkLabels1 @@ -40,13 +28,8 @@ (f : rest) -> 'l' : toUpper f : rest _ -> "" in FunD n [Clause [] (NormalB (- AppE (AppE (VarE (mkName "mkLabel"))- (VarE name)) -- getter+ AppE (AppE (VarE (mkName "label")) (VarE name)) -- getter (LamE [VarP (mkName "b"), VarP (mkName "a")] -- setter (RecUpdE (VarE (mkName "a")) [(name, VarE (mkName "b"))])) )) []]--{-isRec :: Con -> Bool-isRec (RecC _ _) = True-isRec _ = False-}
fclabels.cabal view
@@ -1,15 +1,134 @@ Name: fclabels-Version: 0.3.0-Author: Sebastiaan Visser, Erik Hesselink+Version: 0.4.0+Author: Sebastiaan Visser, Erik Hesselink, Chris Eidhof, Sjoerd Visscher. Synopsis: First class accessor labels.+ Description: First class labels that act as bidirectional records fields. The labels are fully composable and can be used to get, set and modify part of datatypes in a consistent way. The label datatype, conveniently called `:->', is an instance of the- `Category' type class, so is has a proper identity and+ `Category' type class meaning it has a proper identity and composition. The library has support for automatically deriving labels from record selectors that start with an- underscore.+ underscore. Labels can be used in a pure functional setting or+ be applied to mutable state in some state monad.+ .+ To illustrate this package take the following two example+ datatypes (somehow Haddock removes the braces):+ .+ > data Person = Person {+ > _name :: String+ > , _age :: Int+ > , _isMale :: Bool+ > , _place :: Place+ > }+ .+ > data Place = Place {+ > _city+ > , _country+ > , _continent :: String+ > }+ .+ Both are record datatypes with all record labels prefixed with+ an underscore. This underscore is an indication for our+ Template Haskell code to derive labels for these fields.+ Deriving labels can be done with this simple one-liner:+ .+ > $(mkLabels [''Person, ''Place])+ .+ Label function will be generated, label type signatures will+ not. This is actually not that bad, by writing the signatures+ down yourself you will be able to give them documentation,+ something that would be hard otherwise. So, lets give+ functions a signature by hand:+ .+ > name :: Person :-> String+ > age :: Person :-> Int+ > isMale :: Person :-> Bool+ > place :: Person :-> Place+ > city :: Place :-> String+ > country :: Place :-> String+ > continent :: Place :-> String+ .+ These type signatures look very similar to the function types+ for normal record labels, except that the additional colon+ indicates a true first class label. These labels can be used+ to get, set and modify the value and are fully composable.+ .+ Now let look at this example. This 71 old fellow, called Jan,+ is my neighbour and didn't mind using him as an example:+ .+ > jan :: Person+ > jan = Person "Jan" 71 True (Place "Utrecht" "The Netherlands" "Europe")+ .+ When we want to be sure Jan is really as old as he claims we+ can use the @get@ function for labels to get the age out as an+ integer:+ .+ > hisAge :: Int+ > hisAge = get age jan+ .+ Consider he now wants to move to Amsterdam, what better place+ to spend your old days. Using composition when can change the+ city value deep inside the structure:+ .+ > moveToAmsterdam :: Person -> Person+ > moveToAmsterdam = set (city . place) "Amsterdam"+ .+ > moveToAmsterdam jan ==+ > Person "Jan" 71 True (Place "Amsterdam" "The Netherlands" "Europe")+ .+ Composition is done using the dot operator which is part of+ the @Control.Category@ module. Make sure to import this module+ and hide the default @(.)@, @id@ and @mod@ function from the+ Prelude.+ .+ Now, because Jan is an old guy moving to another city is not a+ very easy task, this really takes a while. So it will probably+ take no less than two years before he will actually be+ settled. To reflect this change it might be useful to have a+ first class view on the @Person@ data type that only reveals+ the age and city. This can be done by using a neat+ @Applicative@ functor instance:+ .+ > ageAndCity :: Person :-> (Int, String)+ > ageAndCity = Label $ (,) <$> fst `for` age <*> snd `for` (city . place)+ .+ Because the applicative type class on its own is not very+ capable of expressing bidirectional relations, which we need+ for our labels, the actual instance is defined for an internal+ helper structure called @Point@. Points are a bit more general+ than labels. As you can see above the @Label@ constructor has+ to be used to convert a @Point@ back into a @Label@. The @for@+ function must be used to indicate which partial destructor to+ use for which label in the applicative composition.+ .+ Now that we have an appropriate age+city view on the @Person@+ data type (which is itself a label again), we can use the+ @mod@ function to make Jan move to Amsterdam over exactly two+ years:+ .+ > moveToAmsterdamOverTwoYears :: Person -> Person+ > moveToAmsterdamOverTwoYears = mod ageAndCity (\(a, b) -> (a+2, "Amsterdam"))+ .+ > moveToAmsterdamOverTwoYears jan ==+ > Person "Jan" 73 True (Place "Amsterdam" "The Netherlands" "Europe")+ .+ This package also contains a lens data type that encodes+ bidirectional functions. Just like labels lenses can be+ composed to other lenses using the @Control.Category@ type+ class. Lenses can be used to change the type of a label. The+ @Iso@ type class, which can be seen as a bidirectional+ functor, can be used to apply lenses to labels. For example,+ when we want to threat the age of a person as a string we can+ do the following:+ .+ > ageAsString :: Person :-> String+ > ageAsString :: (show <-> read) `iso` age+ .+ This library might look cryptic at first sight, but give it a+ try it is not that hard.+ Maintainer: Sebastiaan Visser <sfvisser@cs.uu.nl> License: BSD3 License-File: LICENCE