Как использовать линзы для поиска значения на карте, увеличения его или установки значения по умолчанию

При работе с состоянием под названием AppState я хочу отслеживать количество, скажем, экземпляров. Эти экземпляры имеют разные идентификаторы типа InstanceId.

Поэтому мое состояние выглядит так

import           Control.Lens

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

makeLenses ''AppState

Функция для отслеживания количества должна возвращать 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, а вместо этого fmaps больше Maybe.

Предложения по улучшению?


person ruben.moor    schedule 02.09.2015    source источник


Ответы (3)


Способ сделать это с помощью объектива:

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

Ключевым моментом здесь является использование non

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

Это позволяет нам рассматривать отсутствующие элементы из карты instanceCounter как 0.

person glguy    schedule 02.09.2015

Один из способов — использовать <%= оператор. Это позволяет вам изменить цель и вернуть результат:

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)]})
person cchalmers    schedule 02.09.2015
comment
Я чувствую, что еще не до конца понял at... теперь Just ... Just выглядит излишним. Я должен поэкспериментировать еще немного, но это действительно то, что я искал. - person ruben.moor; 02.09.2015
comment
Итак, ключевым моментом для меня было понять изменить функцию, в сигнатуре которой есть функция Maybe a -> Maybe a для установки или отмены значений карты. - person ruben.moor; 03.09.2015
comment
И с точки зрения улучшения, ответы glguy превосходят ваши. Извиняюсь! - person ruben.moor; 03.09.2015

я хотел бы использовать

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

or

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

Я не знаю, есть ли объективный способ сделать то же самое.

person chi    schedule 02.09.2015