How can I use Control.Lens to update the ith element of a list?
Asked Answered
T

2

13

I have some datatypes along the line of

data Outer = Outer { _list :: [ Inner ] }
data Inner = Inner { _bool :: Bool }

using Control.Lens, I can access the _bool of the ith Inner (inside a 'State Outer' monad) like this

boolValue <- gets (^. list . to (!! i) . inner)

I would like to also be able to update this value with something like

list ^. (to (!! i)) ^. inner %= True

However (by my understanding), the 'to' function only creates a getter, not a true lens that can be used as either getter or setter.

So, how can I convert (!! i) into a lens that will allow me to update this field?

Tartu answered 9/6, 2013 at 5:15 Comment(0)
C
17

You can't* turn (!!) into any lens-like thing other than a Getter -- but there's a function to do this sort of thing: ix, for accessing things at indices. It's actually a Traversal, not a Lens -- which, here, just means that it can fail (if the index is out of bounds) -- but as long as the index is in the list, it'll work.

There's another problem, though -- (^.) is also an operator that's used exclusively for getting values. It's incompatible with e.g. (%=), which takes a lens-like thing as its first argument. And: (%=) is for mapping a function over the existing value; if you just want to set, you can use (.=). So you probably want something like:

list . ix i . inner .= True

* There actually is a function that can do this -- it's called upon -- but it uses wonderful evil black magic and you shouldn't use it, at least not for this (and probably not for any real code).

Coveney answered 9/6, 2013 at 5:33 Comment(3)
could you clarify what the difference is between ix and element? I've gone with Gabriel's element since it seems simpler, taking an Int instead of an Index. From the docs you linked, it seems ix is more general in what it allows - is it strictly more general?Tartu
I didn't think of element. It isn't exactly that one is more general than the other... ix is a type class that has a bunch of instances for indexing with particular index types (for example, lists with Int, Maps with their index type, functions with their domain). element takes any Traversable type and counts left-to-right with an Int index. In this case they happen to coïncide.Coveney
I haven't tested it, but I would guess that ix is a bit more efficient for this particular case (mostly because I haven't figured out a way to make Indexing generate good code... :-( Someone else is welcome to try). It's also a somewhat ad-hoc class.Coveney
B
10

Use element, which is a Traversal to the specified list element:

list . element i . inner %= True :: Outer -> Outer

If you want to get the list element you must do so using a Maybe since the list element might not be there:

myList :: Outer

myList ^? list . element i . inner :: Maybe Bool
Barrada answered 9/6, 2013 at 5:26 Comment(2)
Note that %= gives you a State action for modifying a state, i.e. ... :: State Outer (). (Also, I assume the question meant .= here.)Coveney
Yeah, I mean %~, but I think your answer is better anyway.Barrada

© 2022 - 2024 — McMap. All rights reserved.