Reputation: 983
For my purposes, I need a map-like container with 'properties' of some kind. It is possible that my objects can have different properties. To access the properties, I decided to use Control.Lens, it's very interesting. But I can't find a lens-like way for such logic: on access to property, if it doesn't exist, add default value instead, and return the new one. But if it's there, just use it.
In other words, the prop_test should return True:
type PropertyMap = Map.Map Int String
data Properties = Properties { _propertyMap :: PropertyMap }
deriving (Eq)
makeLenses ''Properties
emptyProperties = Properties Map.empty
propertyLens pIndex = propertyMap . at pIndex . traverse
property1 = propertyLens 1
property2 = propertyLens 2
property3 = propertyLens 3
obj1Properties :: State Properties ()
obj1Properties = do
property1 .= "Property1 value"
property2 .= "Property2 value"
obj2Properties :: State Properties ()
obj2Properties = do
property1 .= "Property1 value"
property3 .= "Property3 value"
prop_test = op1 /= emptyProperties
where
op1 = execState obj1Properties emptyProperties
But now, op1 is equal to emptyProperties. For default value, I could use Data.Default. How can I handle this? Or should I use another method maybe? For example, a wrapper function in State monad which unpacks and checks the properties existence for me.
Also, could you give links to real world examples of Control.Lens (or another lens package), please?
Upvotes: 2
Views: 1560
Reputation: 983
Thanks to John F. Miller answer, being thinking about it, I realized, I do need the wrapper functions to simplify using of properties. So, the code will be:
type PropertyMap = Map.Map Int String
data Properties = Properties { _propertyMap :: PropertyMap }
deriving (Eq, Show)
emptyProperties = Properties Map.empty
makeLenses ''Properties
newtype PAccessor = PAccessor { pKey :: Int }
property1 = PAccessor 1
property2 = PAccessor 2
property3 = PAccessor 3
(|=) accessor v = do
ps <- get
put $ propertyMap . at (pKey accessor) ?~ v $ ps
setProperty = (|=)
getProperty accessor = do
ps <- get
return $ ps ^. propertyMap . ix (pKey accessor)
maybeProperty accessor = do
ps <- get
return $ ps ^. propertyMap . at (pKey accessor)
obj1Properties :: State Properties String
obj1Properties = do
property1 |= "Property1 value"
property2 |= "Property2 value"
p1Val <- getProperty property1
p3Val <- getProperty property3 -- Returns default value (empty string)
return (p1Val ++ p3Val)
obj2Properties :: State Properties String
obj2Properties = do
property2 |= "Property2 value"
property3 |= "Property3 value"
Nothing <- maybeProperty property1
Just p2Val <- maybeProperty property2
return p2Val
expectedProps1 = Properties $ Map.fromList [ (1, "Property1 value")
, (2, "Property2 value") ]
expectedProps2 = Properties $ Map.fromList [ (2, "Property2 value")
, (3, "Property3 value") ]
prop_test1 = (props == expectedProps1) && (val == "Property1 value")
where
(val, props) = runState obj1Properties emptyProperties
prop_test2 = (props == expectedProps2) && (val == "Property2 value")
where
(val, props) = runState obj2Properties emptyProperties
prop_test3 = val == "Property2 value"
where
val = evalState obj2Properties emptyProperties
All test functions pass, i.e., results are equal to True.
As a bonus, the code shows the difference between ix
and at
. I'll try to improve this solution, but it looks acceptable for now.
Upvotes: 0
Reputation: 27217
If you make Properties
and instance of 'Monoid' with mempty
equal to emptyProperties
and mappend
something sensible like the union of the two maps then the Lens library will just do the right thing.
The alternative is to use one of the combinators specifically designed for traversals (lenses that can return 0 or more results) and deal with the missing case then. See the documentation for (^?)
which would produce a Maybe t
or (^..)
which will give you [t]
.
Upvotes: 3