Wojciech Danilo
Wojciech Danilo

Reputation: 11803

How to avoid default return value when accessing a non-existent field with lenses?

I love Lens library and I love how it works, but sometimes it introduces so many problems, that I regret I ever started using it. Lets look at this simple example:

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data Data = A { _x :: String, _y :: String }
          | B { _x :: String }

makeLenses ''Data

main = do
    let b = B "x"    
    print $ view y b

it outputs:

""

And now imagine - we've got a datatype and we refactor it - by changing some names. Instead of getting error (in runtime, like with normal accessors) that this name does not longer apply to particular data constructor, lenses use mempty from Monoid to create default object, so we get strange results instead of error. Debugging something like this is almost impossible. Is there any way to fix this behaviour? I know there are some special operators to get the behaviour I want, but all "normal" looking functions from lenses are just horrible. Should I just override them with my custom module or is there any nicer method?

As a sidenote: I want to be able to read and set the arguments using lens syntax, but just remove the behaviour of automatic result creating when field is missing.

Upvotes: 10

Views: 342

Answers (3)

Davorak
Davorak

Reputation: 7444

It sounds like you just want to recover the exception behavior. I vaguely recall that this is how view once worked. If so, I expect a reasonable choice was made with the change.

Normally I end up working with (^?) in the cases you are talking about:

> b ^? y
Nothing

If you want the exception behavior you can use ^?!

> b ^?! y
"*** Exception: (^?!): empty Fold

I prefer to use ^? to avoid partial functions and exceptions, similar to how it is commonly advised to stay away from head, last, !! and other partial functions.

Upvotes: 5

glguy
glguy

Reputation: 1090

You can do this by defining your own view1 operator. It doesn't exist in the lens package, but it's easy to define locally.

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data Data = A { _x :: String, _y :: String }
          | B { _x :: String }

makeLenses ''Data

newtype Get a b = Get { unGet :: a }

instance Functor (Get a) where
  fmap _ (Get x) = Get x

view1 :: LensLike' (Get a) s a -> s -> a
view1 l = unGet . l Get

works :: Data -> String
works = view1 x

-- fails :: Data -> String
-- fails = view1 y

-- Bug.hs:23:15:
--     No instance for (Control.Applicative.Applicative (Get String))
--       arising from a use of ‘y’

Upvotes: 1

Tom Ellis
Tom Ellis

Reputation: 9414

Yes, I too have found it a bit odd that view works for Traversals by concatenating the targets. I think this is because of the instance Monoid m => Applicative (Const m). You can write your own view equivalent that doesn't have this behaviour by writing your own Const equivalent that doesn't have this instance.

Perhaps one workaround would be to provide a type signature for y, so know know exactly what it is. If you had this then your "pathological" use of view wouldn't compile.

data Data = A { _x :: String, _y' :: String }
          | B { _x :: String }

makeLenses ''Data

y :: Lens' Data String
y = y'

Upvotes: 1

Related Questions