Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 749
ruben.moor Avatar asked Sep 02 '15 12:09

ruben.moor


3 Answers

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

like image 196
glguy Avatar answered Nov 12 '22 07:11

glguy


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)]})
like image 5
cchalmers Avatar answered Nov 12 '22 06:11

cchalmers


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.

like image 1
chi Avatar answered Nov 12 '22 06:11

chi