Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning ‘Nothing’ if an indexed list traversal doesn’t match

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.

like image 706
Jon Purdy Avatar asked Apr 25 '21 02:04

Jon Purdy


People also ask

What will index return if not found?

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.

Is there a GET method for lists Python?

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.

What are the items in a list called?

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.


1 Answers

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.


Some Options

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.)

like image 83
DDub Avatar answered Oct 04 '22 17:10

DDub