2
votes

Une lentille pour obtenir ou définir un champ d'enregistrement déterminé par un argument d'exécution

J'ai ces types (et plus):

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 }

Je fais le Tennis kata , et dans le cadre de l'implémentation, j'aimerais utiliser certaines fonctions qui me permettent d'obtenir ou de définir les points pour un joueur arbitraire, connu uniquement à l'exécution.

Formellement, j'ai besoin de fonctions comme celles-ci:

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)

Comme démontré, mon problème n'est pas que je ne peux pas implémenter ces fonctions.

Ils le font, cependant, me ressemble lentille , alors je me demande si je pourrais obtenir cette fonctionnalité via la bibliothèque lens ?

La plupart des didacticiels sur les objectifs montrent comment obtenir ou définir une partie nommée particulière d'une structure de données plus grande. Cela ne semble pas tout à fait correspondre à ce que j'essaie de faire ici; j'essaye plutôt d'obtenir ou de définir une sous-partie déterminée au moment de l'exécution.


1 commentaires

Une fonction pointsToPlayer :: Player -> Lens 'PointsData Points serait-elle suffisante? (Je ne peux pas le tester pour le moment, mais je suis sûr qu'une implémentation directe de cela fonctionnerait tout de suite.)


3 Réponses :


5
votes

Vous pouvez créer une fonction qui produit un Lens étant donné un Player , comme ceci:

pts :: PointsData

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

newPts = pts & playerPoints PlayerOne .~ 42

(ceci utilise champ de generic-lens )

L'utilisation serait comme ceci:

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

PS Ou étiez-vous à la recherche d'un champ de PointsData en faisant correspondre le nom du champ au nom du constructeur de Player ? C'est également possible via Generic , mais cela ne semble pas valoir la peine.


1 commentaires

Note de bas de page: cela fonctionne aussi bien avec les lentilles de lentille - la seule différence est la façon dont les deux lentilles sont définies.



6
votes

Une excursion dans des classes de types quelque peu abstraites. Votre PointsData a une relation particulière avec le type Player . C'est un peu comme un Map Player Point , avec la particularité que pour chaque valeur possible de Player , il y a toujours un Point correspondant. D'une certaine manière, PointsData est comme une "fonction réifiée" Player -> Point .

Si nous rendons PointsData polymorphes sur le type de Points , il cadrerait avec le Classe de types Représentable . Nous dirions que PointsData est "représenté" par Player.

Représentable est souvent utile comme interface aux données tabulaires , comme dans le package grids .


Une solution possible serait donc de transformer PointsData en une Map réelle, mais de cacher l'implémentation derrière un constructeur intelligent qui a pris un Player -> Point pour l'initialiser pour toutes les clés possibles (cela correspondrait à la tabulate méthode de Representable).

L'utilisateur ne devrait pas pouvoir supprimer clés de la carte. Mais nous pourrions utiliser le Ixed instance de Map pour fournir des traversées.

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)


1 commentaires

Je trouve que le concept de penser à PointsData comme une carte correspond étroitement à mes pensées originales, et j'avais espéré qu'il y aurait quelque chose de plus étroitement lié à un tel concept. Puisque, cependant, j'essaie d'explorer des moyens de modéliser des domaines problématiques, je ne considère pas qu'il soit approprié de rendre PointsData polymorphe.



1
votes

D'après la réponse de Fyodor Soikin et le commentaire de duplode em>, j'ai fini par utiliser makeLenses de lens et écrire une fonction qui renvoie l'objectif approprié:

score :: Score -> Player -> Score
-- ..
score (Points pd) winner = Points $ pd & playerPoint winner %~ succ
-- ..

Il peut être utilisé comme ce fragment d'une plus grande fonction:

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

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


0 commentaires