Search code examples
haskellrecordstemplate-haskelllenses

Modify Haskell nested record by value


Say I have a nested structure as follows:

data Bar = Bar { _id :: Integer, _bars :: [Bar] }
data Foo = Foo { _bars :: [Bar] }

And I have a Foo with a bunch of Bars with various ids:

foo = Foo [Bar 1 [Bar 2], Bar 3 [Bar 4, Bar 5]]

How do, perhaps using lenses, I modify foo such that Bar 5 becomes Bar 6?

I know I use fclabels to do something like this:

mkLabel ''Foo
mkLabel ''Bar
modify bars (\bars -> ...) foo

But bars can be nested infinitely. How do I locate and modify the Bar with a specified ID?


Solution

  • Yep, lens can do that. The Control.Lens.Plated module contains tools for "Scrap Your Boilerplate"-style programming with self-similar structures like your Bar. The idea is seductively simple: you explain how to find the immediate children of a node (by writing a Traversal' a a) and the library recursively applies that traversal to the whole structure.

    {-# LANGUAGE TemplateHaskell #-}
    
    import Control.Lens    
    
    data Bar = Bar { _lbl :: Int, _bars :: [Bar] } deriving (Show)
    
    makeLenses ''Bar
    
    instance Plated Bar where
        plate = bars.traverse
    

    (If you don't want to implement plate yourself, you can derive Data and leave the instance empty.)

    transform :: Plated a => (a -> a) -> a -> a takes a function which modifies a single node and applies it to the whole structure.

    fiveToSix :: Bar -> Bar
    fiveToSix = transform go
        where go bar
                | bar^.lbl == 5 = bar & lbl .~ 6
                | otherwise = bar
    

    Using the example from your question:

    ghci> let bars = [Bar 1 [Bar 2 []], Bar 3 [Bar 4 [], Bar 5 []]]
    ghci> map fiveToSix bars
    [Bar 1 [Bar 2 []], Bar 3 [Bar 4 [], Bar 6 []]]
    

    As another example, for funzies, let's use cosmos to pull all of the Bar 5s out of a Bar.

    fives :: Bar -> [Bar]
    fives = toListOf $ cosmos.filtered (views lbl (== 5))