Basic error reporting for optics


2020.01.02

When ^? returns Nothing, it is often desired to know why.

Let's define a ^?? operator which returns an Either instead of a Maybe:

newtype ConstEither e r a = ConstEither { getConstEither :: Either e r }
    deriving Functor

infixl 8 ^??
(^??) :: s -> LensLike' (ConstEither e a) s a -> Either e a
whole ^?? f = f (ConstEither . Right) whole & getConstEither

The standard optics (Traversal, Prism, etc) do not work with our new combinator, so let's see how we can define ones which would.

Traversal s t a b is forall f. Applicative f => (a -> f b) -> s -> f t and it uses f's pure in the empty case, so we'll replace the Applicative with a verbose variant which supplies error information in the empty case:

class Apply f => VerboseApplicative e f where
    vpure :: e -> a -> f a

type VerboseTraversal e s t a b =
    forall f.
    VerboseApplicative e f =>
    LensLike f s t a b

type VerbosePrism e s t a b =
    forall p f.
    (Choice p, VerboseApplicative e f) =>
    Optic p f s t a b

type VerboseTraversal' e s a = VerboseTraversal e s s a a
type VerbosePrism' e s a = VerbosePrism e s s a a

-- Verbose optics support for (^.) and (^..)
instance Monoid r => VerboseApplicative e (Const r) where
    vpure _ = pure

-- Verbose optics support for `preview`, aka (#)
instance VerboseApplicative e Identity where
    vpure _ = pure

-- Verbose optics support for our (^??)
instance Apply (ConstEither e r) where
    ConstEither x <.> _ = ConstEither x
instance e ~ e' => VerboseApplicative e (ConstEither e' r) where
    vpure e _ = ConstEither (Left e)

Now we may want an operator to transform optics into verbose optics:

-- Given an error message constructor, turns:
-- * Traversal to VerboseTraversal
-- * Prism to VerbosePrism
verbose ::
    (Profunctor p, VerboseApplicative e f) =>
    (t -> e) ->
    Optic p (Lift f) s t a b ->
    Optic p f s t a b
verbose e t =
    rmap f . t . rmap Other
    where
        f (Other r) = r
        f (Pure r) = vpure (e r) r

-- A fixed variant of transformers:Control.Applicative.Lift -
-- Turns an Apply to an Applicative
-- (transformer's versions Applicative instance requires Applicative f)
data Lift f a = Pure a | Other (f a)
    deriving Functor

instance Apply f => Applicative (Lift f) where
    pure = Pure
    Pure f <*> Pure x = Pure (f x)
    Pure f <*> Other x = Other (f <$> x)
    Other f <*> Pure x = Other (f <&> ($ x))
    Other f <*> Other x = Other (liftF2 ($) f x)

Note that I haven't found how to make verbose also turn a Fold to a verbose variant.

To see our verbose optics in action we'll make some verbose variants of optics from lens-aeson:

type Err = String

v_Value :: (AsValue t, Show t) => VerbosePrism' Err t Value
v_Value = verbose (\x -> "Doesn't parse as JSON: " <> show x) _Value

v_Double :: (ToJSON t, AsNumber t) => VerbosePrism' Err t Double
v_Double = verbose (expectJson "number") _Double

vnth :: (ToJSON t, AsValue t) => Int -> VerboseTraversal' Err t Value
vnth i = verbose (expectJson ("item at index " <> show i)) (nth i)

expectJson :: ToJSON a => String -> a -> Err
expectJson e x =
    "Expected " <> e <> " but found " <>
    Data.ByteString.Lazy.Char8.unpack (encode x)

Now let's see they work:

# Verbose traversals can work like regular traversals

> "[1, \"x\"]" ^? _Value . nth 0 . _Double
Just 1.0
> "[1, \"x\"]" ^? v_Value . vnth 0 . v_Double
Just 1.0
> "[1, \"x\"]" ^? v_Value . vnth 1 . v_Double
Nothing

# But using ^?? rather than ^? we can also get error info

> "[1, \"x\"]" ^?? v_Value . vnth 0 . v_Double
Right 1.0
> "[1, \"x\"]" ^?? v_Value . vnth 1 . v_Double
Left "Expected number but found \"x\""
> "[1, \"x\"]" ^?? v_Value . vnth 2 . v_Double
Left "Expected item at index 2 but found [1,\"x\"]"
> "hello" ^?? v_Value . v_Double
Left "Doesn't parse as JSON: \"hello\""

Notes

In the previous post's discussion, lens's creator Edward Kmett noted that in lens's early days they experimented with a different formulation of error-reporting optics that placed the extra information in Optic p f s t a b's p rather than f, but that with that formulation they ran into problems with inference and that this new formulation may work better.

Request for feedback

Discussion

Image source