10
votes

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.

3
Lenses are mostly meant for pure product types. For sum types, there's Prisms. Perhaps, makeLenses should completely reject multi-constructor ADTs, but I suppose sometimes it's useful that it allows them. And it's pretty easy to remember that you need to watch out when using lenses with multi-constructor types, isn't it? - leftaroundabout
@Wojciech Record types with more than one constructor are usually a bad idea, IMHO. - danidiaz
I suggest to change the title of the question to something like "How to avoid default return value when accessing a non-existent field with lenses?". As it currently stands the title is useless, it's just a rant without much content to be searched for. Since questions should be useful to many people for a long time it's appropriate to use title that can be easily searched for by others. - Bakuriu
Vote very much not to close! This is an interesting and important question. - Tom Ellis
@Bakuriu but it draws in the traffic. :) - Will Ness

3 Answers

5
votes

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.

1
votes

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'
1
votes

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’