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

3

8

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

Shed answered 13/6, 2013 at 12:57 Comment(0)
B
2

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 :)
Blastocyst answered 16/6, 2013 at 0:23 Comment(3)
So, a Prism lets me change the list? I think I have to test that to understand completly, will do that tomorrow!Shed
Works nicely! But as far as I understand, with the use of filtered, pos is no valid Traversal anymore, right? Therefor it's not generally composable with other traversals.Shed
It's indeed not a valid Traversal if you use it to change the name field and thus make the predicate not hold anymore ("rules" may break, as demonstrated in Matvey's answer), but otherwise, it is composable with other traversals.Blastocyst
M
2

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
Mckeehan answered 13/6, 2013 at 13:45 Comment(0)
L
2

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.

Lavernlaverna answered 14/6, 2013 at 11:38 Comment(2)
I think I have to read some more about lenses to understand this, but thank you for pointing this out! I assume there is nothing like a Traversal which can change the structure of the traversed thing?Shed
That's correct, there's no such thing. The problem is you can't have any nice laws for it.Lavernlaverna
B
2

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 :)
Blastocyst answered 16/6, 2013 at 0:23 Comment(3)
So, a Prism lets me change the list? I think I have to test that to understand completly, will do that tomorrow!Shed
Works nicely! But as far as I understand, with the use of filtered, pos is no valid Traversal anymore, right? Therefor it's not generally composable with other traversals.Shed
It's indeed not a valid Traversal if you use it to change the name field and thus make the predicate not hold anymore ("rules" may break, as demonstrated in Matvey's answer), but otherwise, it is composable with other traversals.Blastocyst

© 2022 - 2024 — McMap. All rights reserved.