如何使用镜头在地图中查找值,增加或将其设置为默认值

时间:2015-09-02 12:34:04

标签: haskell state-monad lenses

在处理名为AppState的状态时,我想跟踪实例的数量。这些实例具有InstanceId类型的不同ID。

因此,我的州看起来像这样

import           Control.Lens

data AppState = AppState
  { -- ...
  , _instanceCounter :: Map InstanceId Integer
  }

makeLenses ''AppState

如果之前没有计算具有给定id的实例,那么跟踪计数的函数应该产生1,否则n + 1

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

虽然上面的代码有效,但希望有一种方法可以改进它。我不喜欢的是:

  • 我必须两次唤醒地图instanceCounter(首先设置,然后获取值)
  • 我使用的fromMaybe始终为Just,因此我不妨使用fromJust
  • 我不会在incOrSetToOne中使用镜头进行查找和插入。原因是at不允许处理lookup产生Nothing而是fmap超过Maybe的情况。

改进建议?

3 个答案:

答案 0 :(得分:8)

使用镜头的方法是:

 countInstances :: InstanceId -> State AppState Integer
 countInstances instanceId = instanceCounter . at instanceId . non 0 <+= 1

这里的关键是使用non

 non :: Eq a => a -> Iso' (Maybe a) a

这允许我们将instanceCounter Map中的缺失元素视为0

答案 1 :(得分:4)

一种方法是使用<%=运算符。它允许您更改目标并返回结果:

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)]

具有逻辑上始终匹配的“部分”模式。

> 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)]})

答案 2 :(得分:1)

我会用

incOrSetToOne = Map.alter (Just . maybe 1 succ) instanceId

incOrSetToOne = Map.alter ((<|> Just 1) . fmap succ) instanceId

我不知道是否有一种透明的方式来做同样的事情。