2

I have these types (and more):

data Player = PlayerOne | PlayerTwo deriving (Eq, Show, Read, Enum, Bounded)

data Point = Love | Fifteen | Thirty deriving (Eq, Show, Read, Enum, Bounded)

data PointsData =
  PointsData { pointsToPlayerOne :: Point, pointsToPlayerTwo :: Point }
  deriving (Eq, Show, Read)

I'm doing the Tennis kata, and as part of the implementation, I'd like to use some functions that enable me to get or set the points for an arbitrary player, only known at runtime.

Formally, I need functions like these:

pointFor :: PointsData -> Player -> Point
pointFor pd PlayerOne = pointsToPlayerOne pd
pointFor pd PlayerTwo = pointsToPlayerTwo pd

pointTo :: PointsData -> Player -> Point -> PointsData
pointTo pd PlayerOne p = pd { pointsToPlayerOne = p }
pointTo pd PlayerTwo p = pd { pointsToPlayerTwo = p }

As demonstrated, my problem isn't that I can't implement these functions.

They do, however, look lens-like to me, so I wonder if I could get that functionality via the lens library?

Most of the lens tutorials show how to get or set a particular, named part of a bigger data structure. This doesn't quite seem to fit what I'm trying to do here; rather, I'm trying to get or set a sub-part determined at runtime.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • 3
    Would a function `pointsToPlayer :: Player -> Lens' PointsData Points` be enough? (Can't test it right now, but I'm quite sure a direct implementation of that would work right away.) – duplode Jun 05 '19 at 21:41

3 Answers3

6

An excursion into somewhat abstract typeclasses. Your PointsData has a special relationship with the Player type. It's a bit like a Map Player Point, with the particularity that for every possible value of Player, there's always a corresponding Point. In a way, PointsData is like a "reified function" Player -> Point.

If we make PointsData polymorphic on the type of Points, it would fit with the Representable typeclass. We would say that PointsData is "represented" by Player.

Representable is often useful as an interface to tabular data, like in the grids package.


So one possible solution would be to turn PointsData into an actual Map, but hide the implementation behind a smart constructor that took a Player -> Point function to initialize it for all possible keys (it would correspond to the tabulate method of Representable).

The user should not be able to delete keys from the map. But we could piggyback on the Ixed instance of Map to provide traversals.

import Control.Lens
import Data.Map.Strict -- from "containers"

newtype PointsData = PointsData { getPoints :: Map Player Point } 

init :: (Player -> Point) -> PointsData
init f = PointsData (Data.Map.Strict.fromList ((\p -> (p, f p)) <$> [minBound..maxBound]))


playerPoints :: Player -> Lens' PointsData Point
playerPoints pl = Control.Lens.singular (iso getPoints PointsData . ix pl)
danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • I find that the concept of thinking about `PointsData` as a map aligns closely with my original thoughts, and I had hoped there'd be something more closely related to such a concept. Since, however, I'm trying to explore ways to model problem domains, I don't consider it appropriate to make `PointsData` polymorphic. – Mark Seemann Jun 06 '19 at 18:45
5

You could create a function that produces a Lens given a Player, like this:

playerPoints :: Player -> Lens' PointsData Point
playerPoints PlayerOne = field @"pointsToPlayerOne"
playerPoints PlayerTwo = field @"pointsToPlayerTwo"

(this is using field from generic-lens)

Usage would be like this:

pts :: PointsData

pl1 = pts ^. playerPoints PlayerOne
pl2 = pts ^. playerPoints PlayerTwo

newPts = pts & playerPoints PlayerOne .~ 42

P.S. Or were you looking for picking a field of PointsData by matching field name to Player constructor name? That is also possible via Generic, but doesn't seem worth the trouble.

Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
  • 1
    Footnote: this works just as well with lenses from *lens* -- the only difference is how the two lenses happen to be defined. – duplode Jun 05 '19 at 21:50
1

Based on the answer from Fyodor Soikin and comment from duplode, I ended up using makeLenses from lens and writing a function that returns the appropriate lens:

data PointsData =
  PointsData { _pointsToPlayerOne :: Point, _pointsToPlayerTwo :: Point }
  deriving (Eq, Show, Read)
makeLenses ''PointsData

playerPoint :: Player -> Lens' PointsData Point
playerPoint PlayerOne = pointsToPlayerOne
playerPoint PlayerTwo = pointsToPlayerTwo

It can be used like this fragment of a bigger function:

score :: Score -> Player -> Score
-- ..
score (Points pd) winner = Points $ pd & playerPoint winner %~ succ
-- ..
duplode
  • 33,731
  • 7
  • 79
  • 150
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736