Search code examples
haskellhaskell-lens

Indexed lenses for nested containers


How can I use lenses to obtain keys from multiple levels of nesting?

Consider the following types

data Outer = Outer { _outerMap :: Map String Inner }
data Inner = Inner { _innerMap :: Map Char Int }
makeLenses ''Outer
makeLenses ''Inner

and assume the following example value

example :: Outer
example = Outer $ Map.fromList
  [ ("A", Inner $ Map.fromList [ ('a', 1), ('b', 2), ('c', 3) ])
  , ("B", Inner $ Map.fromList [ ('a', 4), ('b', 6), ('c', 8) ])
  , ("C", Inner $ Map.fromList [ ('a', 5), ('b', 7), ('c', 9) ])
  ]

Using lenses I can flatten example to a [Int] and filter the odd numbers as follows:

>>> example^..outerMap.folded.innerMap.folded.filtered odd
[1,3,5,7,9]

I can annotate the values with the inner key as follows:

>>> example^@..outerMap.folded.innerMap.ifolded.filtered odd
[('a',1),('c',3),('a',5),('b',7),('c',9)]

But how can I use lenses to annotate the values with both the outer and inner keys, to get the following result?

>>> _whatHere example
[(("A",'a'),1),(("A",'c'),3),(("C",'a'),5),(("C",'b'),7),(("C",'c'),9)]

The following attempt still only returns the inner keys:

>>> example^@..outerMap.ifolded.innerMap.ifolded.filtered odd
[('a',1),('c',3),('a',5),('b',7),('c',9)]

And the following attempt doesn't type-check

>>> example^..outerMap.ifolded.withIndex.alongside id (innerMap.ifolded.filtered odd.withIndex)

error:
    • No instance for (Applicative
                         (Control.Lens.Internal.Getter.AlongsideRight
                            (Const (Data.Monoid.Endo [([Char], (Char, Int))])) [Char]))

An implementation without lenses might look something like this:

nolens :: Outer -> [((String, Char), Int)]
nolens =
  filter (odd . snd)
  . foldMap (\(k, i) -> (map (first (k, )) . Map.toList . _innerMap) i)
  . Map.toList
  . _outerMap

Solution

  • Use (<.>). It's just like (.), except it preserves the indices on both the left and the right. (.) itself (and its alias (.>)) preserves only the index of the RHS, unless the RHS is itself index-preserving, in which case the index comes from the LHS. The mnemonic is that the arrows point to the indices you'd like to save.

    >>> example^@..outerMap.ifolded<.>innerMap.ifolded.filtered odd
    [(("A",'a'),1),(("A",'c'),3),(("C",'a'),5),(("C",'b'),7),(("C",'c'),9)]