Getting multiple results from map with "lens"
Asked Answered
S

2

20

Having these imports:

> import Control.Lens
Control.Lens> import qualified Data.Map as Map

and a map value defined as follows:

Control.Lens Map> let m = Map.fromList [('a', 1), ('c', 3), ('b', 2)]

I can get it's elements one by one like so:

Control.Lens Map> view (at 'b') m
Just 2

What I want to know is, having a set of keys such as this:

Control.Lens Map> import qualified Data.Set as Set
Control.Lens Map Set> let keys = Set.fromList ['d', 'c', 'b']

how to construct such a getter (I guess), using which I'll be able to get a set (or a list) of matching elements:

Control.Lens Map Set> view (**???**) m
[3, 2]

Notice that the result contains only 2 elements, because there's no match for a key 'd'.

Slew answered 17/6, 2013 at 9:28 Comment(0)
D
22

The following will work if you only want a getter over multiple fields.

First, you need to make Accessor from lens an instance of Monoid (that instance is in in HEAD, but not released yet already defined in lens >= 4, so you only need to define the instance if you're working with an old version of the library).

import Data.Monoid
import Control.Lens

instance Monoid r => Monoid (Accessor r a) where
  mempty = Accessor mempty
  mappend (Accessor a) (Accessor b) = Accessor $ a <> b

You can then use that instance to combine multiple lenses/traversals into a single traversal:

>>> import qualified Data.Set as S
>>> import qualified Data.Map as M
>>> import Data.Foldable (foldMap)
>>> import Control.Lens
>>> let m = M.fromList [('a',1), ('b',2), ('c',3)]
>>> let k = S.fromList ['b','c','e']
>>> m ^.. foldMap at k
[Just 2,Just 3,Nothing]
>>> m ^.. foldMap ix k
[2,3]

foldMap uses the Monoid instance for Accessor and the Monoid instance for functions.

Diabetic answered 17/6, 2013 at 20:29 Comment(2)
Accept this answer. It's much better than mine.Thrombin
Excellent! I felt it had to be simple. Thank you!Slew
T
5

I think this is the solution:

import Control.Applicative
import Control.Lens
import qualified Data.Map as M
import Data.Monoid hiding ((<>))

empty :: (Applicative f, Monoid a) => (b -> f b) -> (a -> f a)
empty _ _ = pure mempty

(<>)
    :: (Applicative f, Monoid a)
    => ((b -> f b) -> (a -> f a))
    -> ((b -> f b) -> (a -> f a))
    -> ((b -> f b) -> (a -> f a))
(l1 <> l2) f a = mappend <$> (l1 f a) <*> (l2 f a)

Example:

>>> toListOf (at "A" <> at "B" <> at "C") (M.fromList [("A", 1), ("B", 2)])
[Just 1, Just 2, Nothing]

The idea is that a Traversal is a monoid. The correct solution would require newtyping the Traversal.

Edit: Here is the correct Monoid instance with all the newtype shenanigans:

import Control.Applicative
import Control.Lens
import qualified Data.Map as M
import Data.Monoid
import Data.Foldable

newtype Combinable f a b = Combinable { useAll :: (b -> f b) -> (a -> f a) }

instance (Applicative f, Monoid a) => Monoid (Combinable f a b) where
    mempty = Combinable (\_ _ -> pure mempty)
    mappend (Combinable l1) (Combinable l2)
        = Combinable (\f a -> mappend <$> (l1 f a) <*> (l2 f a))

myMap :: M.Map String Int
myMap = M.fromList [("A", 1), ("B", 2)]

myLens :: Traversal' (M.Map String Int) (Maybe Int)
myLens = useAll $ foldMap (Combinable . at) ["A", "B", "C"]

Example:

>>> toListOf myLens myMap
[Just 1,Just 2, Nothing]
Thrombin answered 17/6, 2013 at 19:36 Comment(2)
Thanks! The second solution is very close. Is there a chance of any update to myLens, which will make toListOf myLens myMap emit a proper list as in what catMaybes $ toListOf myLens myMap currently will do?Slew
@NikitaVolkov I think so. Give me a second to try to get it to work.Thrombin

© 2022 - 2024 — McMap. All rights reserved.