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 fmap
s 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