How to use lenses to look up a value in a map, increase it or set it to a default value
Asked Answered
G

3

8

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:

  • I have to evoke the map instanceCounter twice (first for setting, then for getting the value)
  • I use fromMaybe where always Just is expected (so I might as well use fromJust)
  • I don't use lenses for the lookup and insertion in 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?

Gervais answered 2/9, 2015 at 12:34 Comment(0)
O
9

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

Outrider answered 2/9, 2015 at 16:37 Comment(0)
I
5

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)]})
Interventionist answered 2/9, 2015 at 15:19 Comment(3)
I feel like I haven't fully understood at yet ... now the Just ... Just looks redundant. I gotta experiment some more, but that's indeed what I was looking for.Gervais
OK, so the key for me was to understand the alter function that has in its signature a function Maybe a -> Maybe a to set or unset map values.Gervais
And in terms of improvement, glguy's answers tops yours. Sorry!Gervais
I
1

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.

Ingress answered 2/9, 2015 at 14:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.