21

Given the types

data Prisoner = P { _name   :: String
                  , _rank   :: Int
                  , _cereal :: Cereal }

data Cereal = C { _number             :: Int
                , _percentDailyValue  :: Map String Float
                , _mascot             :: String }

I could extract someone's name, rank, and cereal number via pattern matching:

getNameRankAndCerealNumber_0 :: Prisoner -> (String, Int, Int)
getNameRankAndCerealNumber_0 (P { _name=name
                                , _rank=rank
                                , _cereal = C { _number=cerealNumber }}
                             ) = (name, rank, cerealNumber)

Alternately, I could use lenses to extract each part separately

makeLenses ''Cereal
makeLenses ''Prisoner

getNameRankAndCerealNumber_1 :: Prisoner -> (String, Int, Int)
getNameRankAndCerealNumber_1 p = (p ^. name, p ^. rank, p ^. cereal.number)

Is there a way to extract all three simultaneously in a single traversal of the data structure?

Some way to combine Getters, Getter s a -> Getter s b -> Getter s (a,b)?

rampion
  • 87,131
  • 49
  • 199
  • 315
  • Do you mean "serial number" or is this some joke that I'm not getting? – Tom Ellis Nov 03 '14 at 20:34
  • 2
    Tom Ellis: It's just a bad pun. – rampion Nov 03 '14 at 20:41
  • my original inspiration was [the use of pattern matching that this pull request tries to make work with ghc 7.8.*](https://github.com/schell/hdevtools/pull/1). If the code had used field extractors or lenses, it wouldn't have been needed to be fixed. – rampion Nov 03 '14 at 20:51
  • Mad Physicist: [done](http://stackoverflow.com/a/26793455/9859). Is that what meta.stackoverflow recommends for this scenario? – rampion Nov 07 '14 at 03:18

2 Answers2

25

We can use the Applicative instance of the ReifiedGetter newtype from Control.Lens.Reified:

runGetter $ (,) <$> Getter number <*> Getter mascot

In general, the newtypes in Control.Lens.Reified offer a lot of very useful instances for getters and folds.

Note#1: Notice that we are combining the lenses as getters, and getting a getter in return. You can't obtain a composite lens in this way, as there would be problems if their "focuses" overlap. What could be the proper setter behaviour in that case?

Note#2: The alongside function lets you combine two lenses, getting a bona-fide lens that works on the two halves of a product. This is different form the previous case because we can be sure the lenses don't overlap. alongside comes in handy when your type is a tuple or has an isomorphism to a tuple.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
7

Fleshing out danidiaz's answer above, I was able to construct a Getter Prisoner (String, Int, Int) using ReifiedGetter:

getNameRankAndCerealNumber_2 :: Prisoner -> (String, Int, Int)
getNameRankAndCerealNumber_2 = p ^. nameRankAndCerealNumber_2

nameRankAndCerealNumber_2 :: Getter Prisoner (String, Int, Int)
nameRankAndCerealNumber_2 = runGetter ((,,) <$> Getter name <*> Getter rank <*> Getter (cereal.number))

And a Lens' Prisoner (String, Int, Int) using alongside, though I had to manually construct Iso's between Prisoner and an HList [String, Int, Int] and between HList [a,b,c] and (a,b,c).

getNameRankAndCerealNumber_3 :: Prisoner -> (String, Int, Int)
getNameRankAndCerealNumber_3 p = p ^. nameRankAndCerealNumber_3

setNameRankAndCerealNumber_3 :: (String, Int, Int) -> Prisoner -> Prisoner
setNameRankAndCerealNumber_3 t p = p & nameRankAndCerealNumber_3 .~ t

nameRankAndCerealNumber_3 :: Lens' Prisoner (String, Int, Int)
nameRankAndCerealNumber_3 = hlist . alongside id (alongside id number) . triple
  where triple :: Iso' (a,(b,c)) (a,b,c)
        triple = dimap (\(a,(b,c)) -> (a,b,c)) (fmap $ \(a,b,c) -> (a,(b,c)))
        hlist :: Iso' Prisoner (String, (Int, Cereal))
        hlist = dimap (\(P n r c) -> (n,(r,c))) (fmap $ \(n,(r,c)) -> P n r c)

There may be an easier way to do this, but that's another question.

Community
  • 1
  • 1
rampion
  • 87,131
  • 49
  • 199
  • 315