Using lenses to view a Map as key-value pairs?

201 views Asked by At

The traverse lens (is it a lens?) allows looking at Map key value in a value-by-value basis. For example:

import Data.Map
import Control.Lens

simpleMap :: Map Int Char
simpleMap = fromList [(1, 'a'), (2, 'b')]

nextCharAll :: Map Int Char -> Map Int Char
nextCharAll = traverse %~ succ

nextCharAll simpleMap is fromList [(1, 'b'), (2, 'c')].

What kind of traversal/lens can I use to do something like the following?

nextIntNextChar :: Map Int Char -> Map Int Char
nextIntNextChar = 
    asKeyValuePairs 
    . (_key %~ (+1)) 
    . (_value %~ succ)
2

There are 2 answers

1
snak On

I'm not sure if you like it, but one of the ways is to convert it to a list back and forth with Iso.

nextIntNextChar :: Map Int Char -> Map Int Char
nextIntNextChar = iso toList fromList . traverse %~ bimap (+1) succ
0
K. A. Buhr On

The lens package does provide a mechanism for dealing with keyed data structures, though it is suitable for operations that need read access to the keys, not for modifying the keys. So, it would not help much with writing nextIntNextChar (see below), but you might find it useful for other operations that traverse key-value pairs.

This facility is provided by indexed optics from Data.Lens.Indexed. Note that this is distinct from the at and ix operations from Data.Lens.At. The latter allow you to focus on elements of a keyed data structure by index, while the former allow you to associate an "index" with a particular traversal or other optic that is carried along through lens operations and can be accessed or combined with other indexes as needed.

For example, if you wanted a lens operation to succ the Chars that are indexed by even Int keys, you could write:

succEvenChars :: Map Int Char -> Map Int Char
succEvenChars m = m & itraversed.indices even %~ succ

Here, itraversed constructs an indexed traversal of the Map using the Int keys as the index. The indices optic selects the elements whose indices match a particular predicate. The resulting indexed traversal can be used with a modification operation %~ in the usual way.

(A warning: using traversed in place of itraversed type checks but produces a different answer. The traversed optic creates an indexed traversal as well, but the index is not the Int key from the map; it's an Int index that counts elements in traversal order starting from zero. For this example, the first element, with even index 0 but having an odd-valued key of 1, is the one that has succ applied.)

Operations are provided to modify the index, but that's just a copy of the key, so modifying it doesn't rekey the original data structure So, an implementation of nextIntNextChar that used indexed traversals wouldn't be very exciting. Something like:

nextIntNextChar :: Map Int Char -> Map Int Char
nextIntNextChar m = fromList (m ^.. reindexed succ itraversed.to succ.withIndex)

This uses itraversed to construct an indexed traversal using the keys as indexes, applies reindexed succ to this indexed traversal to transform the indexes, uses the to succ optic to transform the values, and the withIndex optic to produce key-value pairs. Viewing (^..) the resulting optic produces a list of new key-value pairs which can then be loaded up into a new map with fromList. It's a lens solution, true, but it's not appreciably more readable or useful than the solutions provided in other answers and comments.

Note that indexed optics can have the index "loaded" from keys, but the index itself can hold just about anything, it doesn't need to be a key, and in particular doesn't need to uniquely specify a value. As an example, if you have a Tree from Data.Tree, you can use itraversed to produce an indexed traversal using the "default key". This key is a list of integer indexes giving a "path" through the tree, and so its length is the depth of the node.

Using reindexed length to reindex the traversal by depth, you can then perform traversals over all nodes at the same depth:

import Data.Tree
import Data.Tree.Lens
import Control.Lens

t1 :: Tree Char
t1 = Node 'a' [Node 'b' [Node 'c' [],Node 'd' []], Node 'e' [Node 'f' []], Node 'g' []]

main = do
  let nodesAt depth = reindexed length itraversed . index depth
      printTree = putStrLn . drawTree . fmap (:[])

  print $ t1 ^.. nodesAt 2           -- print nodes at depth 2
  printTree $ t1 & nodesAt 1 .~ 'x'  -- set nodes at depth 1