如何使用 Lenses 遍历并赋值给 Map 中的部分(但不是全部)元素

How to use Lenses to traverse and assign to some (but not all) elements in a Map

我一直在尝试将镜头和容器一起使用并取得了一些成功,但我在尝试使用 Data.Map 的过滤遍历时遇到了我的理解限制 - 我可以更改地图中的单个实例或遍历所有实例,但我不知道如何对某些可识别的分区(即范围内的键)进行操作。

基本上我正在尝试用地图做一些类似于 Gabriel Gonzalez 优秀镜头教程用列表做的事情 [1]

这是我的代码的工作框架,其中包含 traverseSome 函数,我不知道如何编写注释掉的代码。非常感谢收到任何帮助!

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveGeneric   #-}
{-# LANGUAGE RankNTypes      #-}

import Control.Lens
import Control.Monad.State
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set

type CharSet = Set.Set Char
type MapOfSets = Map.Map Int CharSet

data DB = DB { _mos  :: MapOfSets } deriving (Show, Eq)

makeLenses ''DB

initDB :: DB
initDB =  DB { _mos  = Map.fromList (zip [1..5] (repeat Set.empty)) }

add2Map :: Int -> CharSet -> State DB ()
add2Map i cs = mos.ix i %= (Set.union cs)

traverseAll :: Traversal' DB CharSet
traverseAll = mos.traversed

add2MapsAll :: CharSet -> State DB ()
add2MapsAll cs = traverseAll %= (Set.union cs)

--        <problematic part>          
{-
traverseSome :: [Int] -> Int -> Traversal' DB MapOfSets
traverseSome ids i = _

add2MapsSome :: [Int] -> CharSet -> State DB ()
add2MapsSome ids cs = mos.(traverseSome ids 2) %= (Set.union cs)
-}         
--        </problematic part>

main :: IO ()
main = do
  let db = initDB
  let bar = Set.fromList ['a'..'g'] :: CharSet
  let baz = Set.fromList ['f'..'m'] :: CharSet
  let quux = Set.fromList ['n'..'z'] :: CharSet

  let db2 = execState (add2Map 5 bar) db
  let db3 = execState (add2MapsAll baz) db
  -- let db4 = execState (add2MapsSome [1,3] quux) db

  print db2
  print db3
  -- print db4

[1] http://www.haskellforall.com/2013/05/program-imperatively-using-haskell.html

我假设你的意思是

traverseSome :: [Int] -> Traversal' DB CharSet

这是一个更通用的版本

keys :: Ord k => [k] -> IndexedTraversal' k (Map.Map k a) a
keys ks f m = go ks <&> \m' -> foldr (uncurry M.insert) m m'
  where
    go []     = pure []
    go (i:is) = case Map.lookup i m of
                  Just a  -> (:) . (,) i <$> indexed f i a <*> go is
                  Nothing -> go is

这与 Data.Vector.Lens 中的 ordinals 非常相似(我的版本没有重复项,因此请确保列表中没有重复项)。 go 遍历索引列表并在地图中查找它们,同时添加索引。 foldr 位遍历已编辑元素的列表,然后 inserts 它们返回到原始地图。

你可以把你的写成

traverseSome :: [Int] -> IndexedTraversal' Int DB CharSet
traverseSome is = mos . keys is

add2MapsSome :: [Int] -> CharSet -> State DB ()
add2MapsSome is cs = traverseSome is %= Set.union cs

如果你确实想要

traverseSome :: [Int] -> Lens' DB MapOfSets

这可以写成(注意你不应该向 Map 添加新键,否则你会违反镜头法则)

submap :: Ord k => [k] -> Lens' (Map.Map k a) (Map.Map k a)
submap ks f m = f (Map.fromList as) <&> (<> m)
  where as = Maybe.mapMaybe (\i -> (,) i <$> Map.lookup i m) ks

可以用来写keys(但效率会较低,因为你做了一个中间Map):

keys :: Ord k => [k] -> IndexedTraversal' k (Map k a) a
keys ks = submap ks . itraversed

编辑:没有中间列表的版本:

keys :: Ord k => [k] -> IndexedTraversal' k (Map.Map k a) a
keys ks f m = go ks
  where
    go []     = pure m
    go (i:is) =
      case Map.lookup i m of
        Just a  -> Map.insert i <$> indexed f i a <*> go is
        Nothing -> go is

Map 是一个instance of TraversableWithIndex, so you can use itraversed to also traverse the keys. indices 可以用来缩小键的范围。

traverseSome :: [Int] -> Traversal' DB CharSet
traverseSome ids = mos . itraversed . indices (`Set.member` idSet) where
  idSet = Set.fromList ids

注意这里的itraversedtraversed是不一样的。 traversed 始终按元素的序号位置进行索引,而 itraversed 可能会根据数据结构按各种键类型进行索引。