Значение по умолчанию, если его нет на карте

Для моих целей мне нужен контейнер, похожий на карту, с какими-то «свойствами». Не исключено, что мои объекты могут иметь разные свойства. Для доступа к свойствам я решил использовать Control.Lens, это очень интересно. Но я не могу найти подобный линзе способ для такой логики: при доступе к свойству, если оно не существует, вместо этого добавьте значение по умолчанию и верните новое. Но если он есть, просто используйте его.

Другими словами, prop_test должен возвращать True:

type PropertyMap = Map.Map Int String
data Properties = Properties { _propertyMap :: PropertyMap }
  deriving (Eq)

makeLenses ''Properties

emptyProperties = Properties Map.empty

propertyLens pIndex = propertyMap . at pIndex . traverse

property1 = propertyLens 1
property2 = propertyLens 2
property3 = propertyLens 3

obj1Properties :: State Properties ()
obj1Properties = do
    property1 .= "Property1 value"
    property2 .= "Property2 value"

obj2Properties :: State Properties ()
obj2Properties = do
    property1 .= "Property1 value"
    property3 .= "Property3 value"

prop_test = op1 /= emptyProperties
    where
        op1 = execState obj1Properties emptyProperties

Но теперь op1 равен emptyProperties. В качестве значения по умолчанию я мог бы использовать Data.Default. Как мне с этим справиться? Или мне стоит использовать другой метод? Например, функция-оболочка в монаде State, которая распаковывает и проверяет наличие свойств для меня.

Также не могли бы вы дать ссылки на реальные примеры Control.Lens (или другого пакета линз)?


person Alexander Granin    schedule 15.09.2013    source источник


Ответы (2)


Если вы сделаете Properties и экземпляр 'Monoid' с mempty равным emptyProperties и mappend, что-то разумное, например объединение двух карт, тогда библиотека Lens просто сделает правильные вещи.

Альтернативой является использование одного из комбинаторов, специально разработанных для обходов (линзы, которые могут возвращать 0 или более результатов), и затем работать с отсутствующим случаем. См. Документацию для (^?), который даст Maybe t или (^..), что даст вам [t].

person John F. Miller    schedule 15.09.2013
comment
На педантичной ноте, ^.. и ^. предназначены для Fold, из которых Traversal являются суперсетами. - person Daniel Gratzer; 15.09.2013

Благодаря ответу Джона Ф. Миллера, подумав об этом, я понял, что мне действительно нужны функции оболочки для упрощения использования свойств. Итак, код будет:

type PropertyMap = Map.Map Int String
data Properties = Properties { _propertyMap :: PropertyMap }
  deriving (Eq, Show)

emptyProperties = Properties Map.empty

makeLenses ''Properties

newtype PAccessor = PAccessor { pKey :: Int }

property1 = PAccessor 1
property2 = PAccessor 2
property3 = PAccessor 3

(|=) accessor v = do
    ps <- get
    put $ propertyMap . at (pKey accessor) ?~ v $ ps

setProperty = (|=)

getProperty accessor = do
    ps <- get
    return $ ps ^. propertyMap . ix (pKey accessor)

maybeProperty accessor = do
    ps <- get
    return $ ps ^. propertyMap . at (pKey accessor)

obj1Properties :: State Properties String
obj1Properties = do
    property1 |= "Property1 value"
    property2 |= "Property2 value"
    p1Val <- getProperty property1
    p3Val <- getProperty property3 -- Returns default value (empty string)
    return (p1Val ++ p3Val)

obj2Properties :: State Properties String
obj2Properties = do
    property2 |= "Property2 value"
    property3 |= "Property3 value"
    Nothing <- maybeProperty property1
    Just p2Val <- maybeProperty property2
    return p2Val

expectedProps1 = Properties $ Map.fromList [ (1, "Property1 value")
                                           , (2, "Property2 value") ]

expectedProps2 = Properties $ Map.fromList [ (2, "Property2 value")
                                           , (3, "Property3 value") ]

prop_test1 = (props == expectedProps1) && (val == "Property1 value")
    where
        (val, props) = runState obj1Properties emptyProperties

prop_test2 = (props == expectedProps2) && (val == "Property2 value")
    where
        (val, props) = runState obj2Properties emptyProperties

prop_test3 = val == "Property2 value"
    where
        val = evalState obj2Properties emptyProperties

Все тестовые функции проходят, т.е. результаты равны True. В качестве бонуса код показывает разницу между ix и at. Я постараюсь улучшить это решение, но пока оно выглядит приемлемым.

person Alexander Granin    schedule 15.09.2013
comment
См. Также: stackoverflow.com/questions/18414177/ - person John F. Miller; 16.09.2013