Lens package with algebraic types
Asked Answered
C

1

14

I was coding with with the lens package. Everything was going fine until I tried to access a certain field on an algebraic type:

import Control.Lens

data Type = A { _a :: Char } | B

makeLenses ''Type

test1 = _a (A 'a')
test2 = (A 'a') ^. a

No instance for (Data.Monoid.Monoid Char)
  arising from a use of `a'
Possible fix:
  add an instance declaration for (Data.Monoid.Monoid Char)
In the second argument of `(^.)', namely `a'
In the expression: (A 'a') ^. a
In an equation for `test2': test2 = (A 'a') ^. a

I could just go with _a, but the datatype in my real program is much deeper and I kind of intended on using lens to lower the amount of work I have to do. I have been looking over the lens library but there's so much there, and I'm not sure if he's dealt with this scenario or it is just something the lens library doesn't support.

As a side note, if I actually use a monoid like String for the datatype instead of Char, it then compiles and gives the right answer, I have no idea why.

Edit: After reading hammar's comment, I tried this and this works:

test2 = (A 'a') ^? a
test3 = B ^? a

But it is kind of weird to get a maybe out of that for something that has to exist.

Colangelo answered 25/4, 2013 at 3:33 Comment(7)
Not too familiar with the inner workings of the lens package, but: Consider what B ^. a should return. It has to pick something, so it tries to use mempty as a default instead of throwing an exception like _a B does.Humphries
I had no idea that _a B would compile. I had a bug in my code that I would not have realized was there until runtime and there was no warning about it even with Wall. So um, I guess I'll use ^? and the maybe function and that does exactly what I need.Colangelo
Actually on second thought, I've come to the realization that accessor methods on algebraic types is just a bad idea. I'm surprised it is even allowed. I had so many bugs in my program that I just cleaned up.Colangelo
It's not bad if you have the same selector for every summand, e.g. data Foo = One { x :: Int } | Two { x :: Int }. But otherwise, yes, it's a partial function, and it's a shame that it's so easy to introduce it without warnings.Skiles
To expand on what @Humphries said: lens has several different lensy types. A Lens is a reference to exactly one value "inside" another value. A Traversal is a reference to zero-or-more values. As such, using (^.) with a Traversal requires a Monoid instance to supply a value (in case there are none) or to combine values (in case there's more than one). (^?) is a convenience function that gives you the first value, if there are any. To avoid partiality, makeLenses generates a Traversal instead of a crashing Lens. Unfortunately this can lead to a confusing message about Monoid.Skiles
Isn't this what Prism is supposed to do? I'm surprised nobody has mentioned that.Cleromancy
@GabrielGonzalez Yes, in this case you can also make an A Prism, but I thought that was a bit out of scope here. E.g. if you wrote data Foo = A { _a :: Int, _b :: Char } | B, then you wouldn't have prisms, just (affine) traversals. (You could still have a Prism' Foo (Int,Char) but that's a bit far from the original question. Also, then I'd have to explain prisms!)Skiles
C
6

Just so that this is answered, my problem was that I had an algebraic type where some fields were in common between the different constructors but there was a couple fields that weren't shared would die in runtime if I tried to use them.

data Exercise =
  BarbellExercise {
    name   :: String,
    weight :: Int,
    reps   :: Int
  } |
  BodyWeightExercise {
    name   :: String,
    reps   :: Int
  }

exer1 = BarbellExercise "Squats" 235 15
exer2 = BarbellExercise "Deadlifts" 265 15
exer3 = BodyWeightExercise "Pullups" 12
exer4 = BarbellExercise "Overhead Press" 85 15

workout = [exer1, exer2, exer3, exer4]

test = do
  mapM_ displayExercise workout

  where
    displayExercise x = putStrLn $ "Exercise: " ++ (name x) ++ " You must perform " ++ (show $ reps x) ++ "@" ++ (show $ weight x)

This compiles but dies runtime if I make the mistake of using the weight function. Understandable mistake. When lenses uses template haskell to generate instances it notices this and changes its behavior to prevent a mistake. You could remove the field accessors but in my case most of the fields were the same between datatypes. Here's how I should have written the data type once I noticed the fields did not match up:

data Exercise =
  BarbellExercise
   String -- ^ name
   Int    -- ^ reps
   Int    -- ^ weight
     |
  BodyWeightExercise
    String -- ^ name
    Int    -- reps


name :: Exercise -> String
name (BarbellExercise n _ _) = n
name (BodyWeightExercise n _) = n

reps :: Exercise -> Int
reps (BarbellExercise _ r _) = r
reps (BodyWeightExercise _ r) = r

By doing it this way, while it is a little less clean, the error are caught at compile time. By forcing me to write the functions myself I would notice any partial functions as I was writing them.

I do kind of wish ghc would have warned me. It seems like it would be really easy for it to detect such a thing.

Colangelo answered 25/4, 2013 at 13:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.