While working on a state called AppState I want keep track of the number of, say, instances. These instances have distinct ids of type InstanceId.
Therefore my state look likes this
import Control.Lens
data AppState = AppState
{ -- ...
, _instanceCounter :: Map InstanceId Integer
}
makeLenses ''AppState
The function to keep track of counts should yield 1 when no instance with given id has been counted before and n + 1 otherwise:
import Data.Map as Map
import Data.Map (Map)
countInstances :: InstanceId -> State AppState Integer
countInstances instanceId = do
instanceCounter %= incOrSetToOne
fromMaybe (error "This cannot logically happen.")
<$> use (instanceCounter . at instanceId)
where
incOrSetToOne :: Map InstanceId Integer -> Map InstanceId Integer
incOrSetToOne m = case Map.lookup instanceId m of
Just c -> Map.insert instanceId (c + 1) m
Nothing -> Map.insert instanceId 1 m
While the above code works, there is hopefully a way to improve it. What I don't like:
instanceCounter twice (first for setting, then for getting the value)fromMaybe where always Just is expected (so I might as well use fromJust)incOrSetToOne. The reason is that at does not allow to handle the case where lookup yields Nothing but instead fmaps over Maybe.Suggestions for improvement?
The way to do this using lens is:
countInstances :: InstanceId -> State AppState Integer
countInstances instanceId = instanceCounter . at instanceId . non 0 <+= 1
The key here is to use non
non :: Eq a => a -> Iso' (Maybe a) a
This allows us to treat missing elements from the instanceCounter Map as 0
One way is to use the <%= operator. It allows you to alter the target and return the result:
import Control.Lens
import qualified Data.Map as M
import Data.Map (Map)
import Control.Monad.State
type InstanceId = Int
data AppState = AppState { _instanceCounter :: Map InstanceId Integer }
deriving Show
makeLenses ''AppState
countInstances :: InstanceId -> State AppState Integer
countInstances instanceId = do
Just i <- instanceCounter . at instanceId <%= Just . maybe 1 (+1)
return i
initialState :: AppState
initialState = AppState $ M.fromList [(1, 100), (3, 200)]
which has a "partial" pattern that should logically always match.
> runState (countInstances 1) initialState
(101,AppState {_instanceCounter = fromList [(1,101),(3,200)]})
> runState (countInstances 2) initialState
(1,AppState {_instanceCounter = fromList [(1,100),(2,1),(3,200)]})
> runState (countInstances 300) initialState
(201,AppState {_instanceCounter = fromList [(1,100),(3,201)]})
I would use
incOrSetToOne = Map.alter (Just . maybe 1 succ) instanceId
or
incOrSetToOne = Map.alter ((<|> Just 1) . fmap succ) instanceId
I don't know if there's a lensy way to do the same.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With