I'm trying to figure out the cleanest way to modify values nested inside of Maybe
types (or other types for modeling partiality).
Here is the example setup:
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Outer = Outer { _inner :: Maybe Inner }
deriving (Show)
data Inner = Inner { _foo :: Int }
deriving (Show)
makeLenses ''Outer
makeLenses ''Inner
It's pretty easy to do this in a somewhat messy fashion with lens
:
wibble :: Outer -> Maybe Outer
wibble o = do i <- view inner o
let i' = over foo succ i
return $ set inner (Just i') o
To put a point on why this is nasty, here's what I would like to be able to write:
wibble' :: Outer -> Maybe Outer
wibble' = overish inner.foo succ
overish = ???
A failure to look up a field should just make the whole operation fail, rather than making me explicitly check for failure at each point where it might happen.
Any suggestions? I've tried navigating the various lens
modules, but nothing has quite appeared to fit the bill.
You can use over (inner.traverse.foo)
to write to the nested field. This will not report failure the way you want though, because it succeeded in mapping over all 0 targets.
traverse
(from Data.Traversable
, re-exported by Control.Lens
) here gives you a Traversal
for walking over the Maybe
.
You can read from it with (^?)
to see if the target of the lens exists.
We can solve this in several ways by using existing lens combinators to read and write separately, but we could just build such a combinator directly:
import Data.Monoid (Any(..))
import Control.Monad (guard)
overish :: LensLike ((,) Any) s t a b -> (a -> b) -> s -> Maybe t
overish l f s = case l (\a -> (Any True, f a)) s of
(Any r, t) -> t <$ guard r
You could write this with l %%~ \a -> (Any True, f a)
as well.
You can easily check if a Traversal
has no targets by using nullOf
, but this will require two passes and a higher rank type:
overish :: Traversal s t a b -> (a -> b) -> s -> Maybe t
overish l f s = over l f s <$ guard (not (nullOf l s))
This is effectively just checking to see that there is a target and then applying the setter to it if there are targets.
Then you can just use
overish (inner.traverse.foo) succ
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