Change element of list if it holds against some condition or add a new one if not, using Data.Lens

StackOverflow https://stackoverflow.com/questions/17087663

  •  31-05-2022
  •  | 
  •  

Question

I have a list of records and need a function which searches the list for a record with a given name and modify the value of this record OR if no record matches append a new record to the resulting list. Here is my code so far:

import Control.Lens
import Control.Applicative ((<$>), pure)
import Data.List (any)

data SomeRec = SomeRec { _name :: String, _val :: Int }
$(makeLenses ''SomeRec)

_find :: (a -> Bool) -> Simple Traversal [a] a
_find _ _ [] = pure []
_find pred f (a:as) = if pred a
                        then (: as) <$> f a
                        else (a:) <$> (_find pred f as)

changeOrCreate :: [SomeRec] -> String -> (Int -> Int) -> [SomeRec]
changeOrCreate recs nameToSearch valModifier = 
  if (any (\r -> r^.name == nameToSearch) recs)
    then over (_find (\r -> r^.name == nameToSearch)) (over val valModifier) recs
    else recs ++ [SomeRec nameToSearch (valModifier 0)]

It works fine, but I'm wondering if there is a more direct way of writing this using Data.Lens (without the if-construct)? Also, do I have to write the _find function or is there something equivalent in the library?

Update: Here is a Gist of the source to experiment: https://gist.github.com/SKoschnicke/5795863

Was it helpful?

Solution

How about:

changeOrCreate :: String -> (Int -> Int) -> [SomeRec] -> [SomeRec]
changeOrCreate nameToSearch valModifier = 
  pos . val %~ valModifier
  & outside (filtered (not . has pos)) %~ (. newRec)
  where
    pos :: Traversal' [SomeRec] SomeRec
    pos = taking 1 (traversed . filtered (anyOf name (== nameToSearch)))
    newRec = (SomeRec nameToSearch 0 :)

OTHER TIPS

I don't know, but you can write some like

changeOrCreate [] n f = [SomeRec n (f 0)]
changeOrCreate (r:rs) n f | r^.name == n = (over val f) r:rs
                          | otherwise    = r: changeOrCreate rs n f

So, _find is not actually a Traversal:

> [1..10] & over (_find odd) succ . over (_find odd) succ
[2,2,4,4,5,6,7,8,9,10]
> [1..10] & over (_find odd) (succ . succ)
[3,2,3,4,5,6,7,8,9,10]

That's the same sense filtered is not a traversal.

Getting part can be mimicked with filtered (it's okay here since Fold does not have any laws):

> [1..10] ^? _find even
Just 2
> [1..10] ^? _find (> 20)
Nothing
> [1..10] ^? folded . filtered even
Just 2
> [1..10] ^? folded . filtered (> 20)
Nothing

Now, assuming "more direct way" is some clever Traversal: no, that's not possible, Traversals can not modify the structure of the traversed thing.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top