I’m new to lens
and trying to use it to compose many small modifications to a nested structure, which may fail and possibly return additional results:
element -> Maybe element
element -> Maybe (result, element)
How do I modify an inner structure by index, also returning Nothing
if the index isn’t present? If I use traverseOf
+ix
:
type Thing = (String, [Int])
exampleThing :: Thing
exampleThing = ("example", [0, 1])
predMaybe :: Int -> Maybe Int
predMaybe x
| x == 0 = Nothing
| otherwise = Just (pred x)
decrementThingAt :: Int -> Thing -> Maybe Thing
decrementThingAt i = traverseOf (_2 . ix i) predMaybe
> decrementThingAt 1 exampleThing
Just ("example",[0,0])
> decrementThingAt 0 exampleThing
Nothing
Then if the index isn’t present, this silently returns the structure unmodified:
> decrementThingAt 2 exampleThing
Just ("example",[0,1])
Whereas I want to return Nothing
here as well. I’d like to do it “inside” the lens composition if possible. I know I can use preview
/ ^?
“outside” to get a Maybe
according to whether an optic matched any targets:
> preview (_2 . ix 1) exampleThing
Just 1
> preview (_2 . ix 2) exampleThing
Nothing
But I expected to be able to write something like traverseOf (_2 . ix i . previewed) predMaybe
. I see some awkward ways to do it “outside”, like with foldMapOf
:
decrementThingAt i = getFirst . foldMapOf (_2 . ix i) (First . predMaybe)
But is there a way to keep everything in the same pipeline, so that I’m not repeatedly/explicitly disassembling & reassembling the structure?
I also don’t quite see how to combine this with returning additional results. At the moment I’m using StateT
and zoom
like this:
import Control.Lens (_1, zoom)
import Control.Monad.Trans.State (StateT, runStateT)
import Data.List (uncons)
-- NB: uncons :: [a] -> Maybe (a, [a])
pop :: Thing -> Maybe (Char, Thing)
pop = runStateT $ zoom _1 $ StateT uncons
> pop exampleThing
Just ('e',("xample",[0,1]))
> pop ("", [0, 1])
Nothing
But I still don’t know how to make that work with missing indices, for example, using type ThingMaybe = (Maybe String, [Int])
and failing if that Maybe
is Nothing
.
The index() method searches for the first occurrence of the given item and returns its index. If specified item is not found, it raises 'ValueError' exception.
To find an element in the list, use the Python list index() method, The index() is an inbuilt Python method that searches for an item in the list and returns its index. The index() method finds the given element in the list and returns its position.
The values that make up a list are called its elements, or its items. We will use the term element or item to mean the same thing. Lists are similar to strings, which are ordered collections of characters, except that the elements of a list can be of any type.
What you're asking for isn't possible. To see why, let's look at your decrementThingAt
example. Let's say you came up with some traversal that did what you want. That is, you could write
decrementThingAt :: Int -> Thing -> Maybe Thing
decrementThingAt i = traverseOf myTrav predMaybe
such that
> decrementThingAt 1 exampleThing
Just ("example",[0,0])
> decrementThingAt 2 exampleThing
Nothing
Now, let's explore the function:
unknown :: Int -> Thing -> Thing
unknown i = myTrav %~ id
With any normal traversal, unknown n t == t
for all (type-correct) choices of n
and t
, but with myTrav
, that's not clear anymore. If you call unknown 2 exampleThing
, presumably you want it to return ... Nothing
? That doesn't even make sense at the type level.
Looking at it another way, you can't construct a traversal myTrav
for the same reason that no many how many times you run
> traverse predMaybe []
Just []
you'll never get Nothing
. The traversal ix
essentially filters out the elements of the list that you're calling predMaybe
on, and when you ix
on an index that isn't present, it's like you filtered out all elements. There ends up being no call to predMaybe
, so there's no way to return Nothing
.
As you correctly identified, the right way to do what you're looking for is "outside" the traversal. This makes sense: it's the same level that you're declaring the use of predMaybe
, which, after all, is where the Maybe
applicative is coming into play. For example, your use of First
and foldMapOf
works because First
is a monoid with the property that mempty ≈ Nothing
, just as you want.
When it comes to returning additional results, I like using the (a,)
functor:
import Data.Functor.Compose
pop :: Thing -> Maybe (Char, Thing)
pop = getCompose . _1 (Compose . uncons)
-- Or if you prefer using all the lens operators:
-- pop t = getCompose $ t & _1 %%~ (Compose . uncons)
In this case, it's made a little extra complicated by the need to use Compose
, but it still ends up doing what I want.
As for returning Nothing
when indices are missing, you're going to run into the same problem I described above.
For what it's worth, I believe the simplest way to achieve the goal you're looking for is not to do it at the "inside" level but to do it at the outside level, and there's no reason this will end up "repeatedly/explicitly disassembling & reassembling the structure". Consider something like this:
maybeToAny :: Maybe a -> Compose Maybe ((,) Any) a
maybeToAny = Compose . fmap (Any True,)
getIfAny :: Compose Maybe ((,) Any) a -> Maybe a
getIfAny (Compose Nothing) = Nothing
getIfAny (Compose (Just (Any False, _))) = Nothing
getIfAny (Compose (Just (Any True, a))) = Just a
decrementThingAt :: Int -> Thing -> Maybe Thing
decrementThingAt i = getIfAny . traverseOf (_2 . ix i) (maybeToAny . predMaybe)
The idea here is we use the Any
value to check if maybeToAny . predMaybe
was ever called. If it was, then the Any
value will be True
, and we can produce the result value, and otherwise, our traversal must have missed, so we return Nothing
.
If you really want to do this "inside" the optic, We can also make a pseudo-traversal that follows the same principle:
ixj :: Ixed m => Index m -> Traversal' (Maybe m) (IxValue m)
ixj i _ Nothing = pure Nothing
ixj i afb (Just s) =
let t' = getCompose $ s & ix i %%~ Compose . fmap (Any True,) . afb
go (Any True, t) = Just t
go (Any False, _) = Nothing
in go <$> t'
Note that we need the s
type to be Maybe m
instead of just m
so that we can fail. Unfortunately, this is not a true traversal because it doesn't obey the traversal laws (this whole idea of failing is contrary to how optics work). I still managed to come up with something, but it's a little ugly:
finside :: Applicative f => ALens s t a b -> Lens (f s) (f t) (f a) (f b)
finside (cloneLens -> l) = lens (fmap $ getConst . l Const) (liftA2 $ flip (l .~))
decrementThingAt :: Int -> Thing -> Maybe Thing
decrementThingAt i = join . traverseOf (finside _2 . ixj i) predMaybe . Just
(I'm not sure if finside
or something similar already exists in the lens library. I'm also not sure if what it produces will always obey the lens laws.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With