Question

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.

Était-ce utile?

La solution

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
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top