如何使用镜头在地图中查找值、增加值或将其设置为默认值
How to use lenses to look up a value in a map, increase it or set it to a default value
在处理名为 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
.
改进建议?
我会用
incOrSetToOne = Map.alter (Just . maybe 1 succ) instanceId
或
incOrSetToOne = Map.alter ((<|> Just 1) . fmap succ) instanceId
我不知道是否有一种简单的方法可以做到这一点。
一种方法是使用 <%=
运算符。它允许您更改目标和 return 结果:
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)]
它有一个 "partial" 逻辑上应该总是匹配的模式。
> 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)]})
使用镜头的方法是:
countInstances :: InstanceId -> State AppState Integer
countInstances instanceId = instanceCounter . at instanceId . non 0 <+= 1
这里的关键是使用non
non :: Eq a => a -> Iso' (Maybe a) a
这允许我们将 instanceCounter 映射中缺失的元素视为 0
在处理名为 AppState
的状态时,我想跟踪实例的数量。这些实例具有 InstanceId
.
因此我的状态看起来像这样
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
.
改进建议?
我会用
incOrSetToOne = Map.alter (Just . maybe 1 succ) instanceId
或
incOrSetToOne = Map.alter ((<|> Just 1) . fmap succ) instanceId
我不知道是否有一种简单的方法可以做到这一点。
一种方法是使用 <%=
运算符。它允许您更改目标和 return 结果:
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)]
它有一个 "partial" 逻辑上应该总是匹配的模式。
> 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)]})
使用镜头的方法是:
countInstances :: InstanceId -> State AppState Integer
countInstances instanceId = instanceCounter . at instanceId . non 0 <+= 1
这里的关键是使用non
non :: Eq a => a -> Iso' (Maybe a) a
这允许我们将 instanceCounter 映射中缺失的元素视为 0