10

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.

Wojciech Danilo
  • 11,573
  • 17
  • 66
  • 132
  • 7
    `Lens`es are mostly meant for pure product types. For sum types, there's `Prism`s. 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 Dec 19 '14 at 14:16
  • @leftaroundabout Ok, but can I read and write values when using `makePrisms`? I do not read each value as `Maybe` (using `preview`). I just want to read and set values like normal Haskell datatypes allow me to. Is it somehow possible? – Wojciech Danilo Dec 19 '14 at 14:19
  • 4
    @Wojciech Record types with more than one constructor are usually a bad idea, IMHO. – danidiaz Dec 19 '14 at 14:34
  • 8
    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 Dec 19 '14 at 14:59
  • 2
    Vote very much not to close! This is an interesting and important question. – Tom Ellis Dec 19 '14 at 15:45
  • 3
    @Bakuriu but it draws in the traffic. :) – Will Ness Dec 19 '14 at 15:51
  • @WillNess - Does not help future readers trying to answer their question though. – Davorak Dec 19 '14 at 18:21
  • 1
    This is a reasonably clear question and does not deserve to be closed by any means! The only problem was a bad title, which I edited to try to actually describe the problem. – Tikhon Jelvis Dec 19 '14 at 19:13
  • @Davorak correct. (a) I was joking; (b) ah! the title is already edited! good. – Will Ness Dec 19 '14 at 19:46
  • I think the problem is more related to records sum types, than lens. However, the fact that it doesn't return an error (as `_y b` will) is indeed controversial. However it's also sometime handy to not have to catch the error – mb14 Dec 21 '14 at 18:06

3 Answers3

5

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.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
Davorak
  • 7,362
  • 1
  • 38
  • 48
1

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'
Tom Ellis
  • 9,224
  • 1
  • 29
  • 54
1

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’
glguy
  • 1,090
  • 7
  • 8