使用镜头对任意嵌套的数据结构进行功能更新



假设我有一个代表一个保存袋的数据结构,它可以容纳多个项目。用户可以在这个袋子里放另一个保温袋,这个袋子可以装其他袋子,甚至是装有袋子的袋子。是否有用于功能更新任意嵌套袋子的镜头,例如从袋子中的袋子中取出物品 foo,袋子内的袋子?请注意,嵌套级别以及树的总深度是动态的,在编译时未知。像这样和这样的其他问题似乎只处理静态已知的嵌套级别。

我正在寻找的东西可以在 Clojure 中使用更新函数来完成,方法是动态生成一个访问器向量以传递给该函数。

你对"Bag of Holding"的描述并不准确,但我认为这与你的意思很接近。基本思想是使用[Int]遍历到子包中(类似于TreeIxed实例),并使用At实例进行Map编辑项目。

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedLists   #-}
{-# LANGUAGE RankNTypes        #-}
{-# LANGUAGE TypeFamilies      #-}
import           Control.Lens
import qualified Data.Map     as M
data Bag k a = Bag (M.Map k a) [Bag k a]
  deriving (Show)
-- | Lens onto top level items of a bag.
items :: Lens' (Bag k a) (M.Map k a)
items f (Bag k a) = f k <&> k' -> Bag k' a
-- | Use 'At' instance for 'M.Map' to edit top level items.
atItem :: Ord k => k -> Lens' (Bag k a) (Maybe a)
atItem k = items . at k
type instance Index (Bag k a)   = [Int]
type instance IxValue (Bag k a) = Bag k a
instance Ixed (Bag k a) where
  ix is0 f = go is0 where
    -- Use the `Ixed` instance for lists to traverse over
    -- item `i` in the list of bags.
    go (i:is) (Bag m bs) = Bag m <$> ix i (go is) bs
    go _      b          = f b
  {-# INLINE ix #-}
mybag :: Bag String Char
mybag =
  Bag [("a1",'a')] -- ix []
   [ Bag [] []     -- ix [0]
   , Bag []        -- ix [1]
     [ Bag [("foo", 'x'), ("bar",'y')] [] -- ix [1,0]
     , Bag [("FOO", 'X'), ("BAR",'Y')] [] -- ix [1,1]
     ]
  ]

所以现在如果我们想从袋子中删除"FOO"项目[1,1]

> mybag & ix [1,1] . atItem "FOO" .~ Nothing
Bag (fromList [("a1",'a')])
  [Bag (fromList []) []
  ,Bag (fromList [])
     [Bag (fromList [("bar",'y'),("foo",'x')]) []
     ,Bag (fromList [("BAR",'Y')]) []]]

或将"foobar"插入袋子[1,0]

> mybag & ix [1,0] . atItem "foobar" ?~ 'z'
Bag (fromList [("a1",'a')])
  [Bag (fromList []) []
  ,Bag (fromList [])
    [Bag (fromList [("bar",'y'),("foo",'x'),("foobar",'z')]) []
    ,Bag (fromList [("BAR",'Y'),("FOO",'X')]) []]]

实际上,我对Bag的定义只是一个专门的Tree

import Data.Tree
import Data.Tree.Lens
type Bag k a = Tree (M.Map k a)
atItem :: Ord k => k -> Lens' (Bag k a) (Maybe a)
atItem k = root . at k
subBag :: [Int] -> Traversal' (Bag k a) (Bag k a)
subBag (i:is) = branches . ix i . subBag is
subBag _      = id

这可以像以前一样使用subBag而不是ixsubBag的定义可能这样写得更清楚。

实际上您不需要编写任何新函数,因为TreeIxed实例与subBag is . root相同,因此可以通过以下方式进行编辑:

> mybag & ix [1,1] . at "FOO" .~ Nothing

假设Bag数据类型如下:

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleInstances #-}
import Control.Lens
import Control.Lens.Reified
import Data.Monoid
type Item = Int
data Bag = Bag 
    {
        _items :: [Item]
    ,   _bags :: [Bag]
    } deriving (Show)
$(makeLenses ''Bag)
exampleBag :: Bag
exampleBag = Bag [1,2] [Bag [] [], Bag [] [Bag [3] [Bag [0] []]]]

Control.Lens.Reified中,有ReifiedTraversal newtype,用于在容器中存储遍历。我们可以为那些以相同数据类型开始和结束的遍历声明一个Monoid实例:

instance Monoid (ReifiedTraversal s s s s) where
    mempty = Traversal id
    mappend (Traversal t1) (Traversal t2) = Traversal (t1 . t2) 

mappend只是遍历的组成(有点像Endo幺半群的工作方式。

现在,我们可以使用列表在运行时定义从BagBag的遍历:

lensList :: [ReifiedTraversal' Bag Bag]
lensList = 
    [ Traversal $ bags . ix 1
    , Traversal $ bags . ix 0
    , Traversal $ bags . ix 0
    ] 

并对其进行测试:

main :: IO ()
main = print $ over ((runTraversal $ mconcat lensList) . items . ix 0) succ exampleBag

我们还可以为Bag定义一个Plated实例,这将允许我们做一些事情,比如列出层次结构中的所有袋子,或者对袋子执行副态。一个"巴加形态",如果你愿意的话。

最新更新