Question

Disons que j'ai le modèle de données suivant, pour garder une trace des statistiques des joueurs de baseball, des équipes et des entraîneurs:

data BBTeam = BBTeam { teamname :: String, 
                       manager :: Coach,
                       players :: [BBPlayer] }  
     deriving (Show)

data Coach = Coach { coachname :: String, 
                     favcussword :: String,
                     diet :: Diet }  
     deriving (Show)

data Diet = Diet { dietname :: String, 
                   steaks :: Integer, 
                   eggs :: Integer }  
     deriving (Show)

data BBPlayer = BBPlayer { playername :: String, 
                           hits :: Integer,
                           era :: Double }  
     deriving (Show)

Disons maintenant que les managers, qui sont généralement des fanatiques de steaks, veulent manger encore plus de steaks - nous devons donc être en mesure d'augmenter la teneur en steak de l'alimentation d'un manager. Voici deux implémentations possibles pour cette fonction:

1) Cela utilise beaucoup de correspondance de motifs et je dois obtenir tous les arguments pour tous les constructeurs correctement ... deux fois. Il semble que ce ne serait pas très bien mis à l'échelle ou très facile à maintenir / lisible.

addManagerSteak :: BBTeam -> BBTeam
addManagerSteak (BBTeam tname (Coach cname cuss (Diet dname oldsteaks oldeggs)) players) = BBTeam tname newcoach players
  where
    newcoach = Coach cname cuss (Diet dname (oldsteaks + 1) oldeggs)

2) Cela utilise tous les accesseurs fournis par la syntaxe d'enregistrement de Haskell, mais c'est aussi moche et répétitif, et difficile à maintenir et à lire, je pense.

addManStk :: BBTeam -> BBTeam
addManStk team = newteam
  where
    newteam = BBTeam (teamname team) newmanager (players team)
    newmanager = Coach (coachname oldcoach) (favcussword oldcoach) newdiet
    oldcoach = manager team
    newdiet = Diet (dietname olddiet) (oldsteaks + 1) (eggs olddiet)
    olddiet = diet oldcoach
    oldsteaks = steaks olddiet

Ma question est la suivante: est-ce que l'un d'eux est meilleur que l'autre, ou est-il plus préféré au sein de la communauté Haskell? Y a-t-il une meilleure façon de faire cela (pour modifier une valeur profondément dans une structure de données tout en gardant le contexte)? Je ne suis pas préoccupé par l'efficacité, juste l'élégance / la généralité / la maintenabilité du code.

J'ai remarqué qu'il y avait quelque chose pour ce problème (ou un problème similaire?) dans Clojure: update-in - donc je pense que j'essaie de comprendre update-in dans le contexte de la programmation fonctionnelle et Haskell et du typage statique.

Était-ce utile?

La solution

La syntaxe de mise à jour des enregistrements est fournie en standard avec le compilateur:

addManStk team = team {
    manager = (manager team) {
        diet = (diet (manager team)) {
             steaks = steaks (diet (manager team)) + 1
             }
        }
    }

Terrible!Mais il y a un meilleur moyen.Il existe plusieurs packages sur Hackage qui implémentent des références fonctionnelles et des objectifs, ce que vous voulez vraiment faire.Par exemple, avec le package fclabels , vous mettriez des traits de soulignement devant tous vos noms d'enregistrement,puis écrivez

$(mkLabels ['BBTeam, 'Coach, 'Diet, 'BBPlayer])
addManStk = modify (+1) (steaks . diet . manager)

Modifié en 2017 pour ajouter: de nos jours, il existe un large consensus sur le fait que le package lens estune technique de mise en œuvre particulièrement bonne.Bien qu'il s'agisse d'un très gros paquet, il existe également une très bonne documentation et du matériel d'introduction disponibles à divers endroits sur le Web.

Autres conseils

Voici comment vous pouvez utiliser des combinateurs d'éditeur sémantique (SEC), comme l'a suggéré Lambdageek.

Tout d'abord, quelques abréviations utiles:

type Unop a = a -> a
type Lifter p q = Unop p -> Unop q

Le Unop ici est un "éditeur sémantique", et le Lifter est le combinateur de l'éditeur sémantique. Quelques haltérophiles:

onManager :: Lifter Coach BBTeam
onManager f (BBTeam n m p) = BBTeam n (f m) p

onDiet :: Lifter Diet Coach
onDiet f (Coach n c d) = Coach n c (f d)

onStakes :: Lifter Integer Diet
onStakes f (Diet n s e) = Diet n (f s) e

Maintenant, composez simplement les SEC pour dire ce que vous voulez, à savoir ajouter 1 aux enjeux du régime du manager (d'une équipe):

addManagerSteak :: Unop BBTeam
addManagerSteak = (onManager . onDiet . onStakes) (+1)

En comparaison avec l'approche SYB, la version SEC nécessite un travail supplémentaire pour définir les SEC, et je n'ai fourni que celles nécessaires dans cet exemple.La SEC permet une application ciblée, ce qui serait utile si les joueurs avaient des régimes mais nous ne voulions pas les modifier.Peut-être y a-t-il aussi une jolie façon SYB de gérer cette distinction.

Modifier: Voici un style alternatif pour les SEC de base:

onManager :: Lifter Coach BBTeam
onManager f t = t { manager = f (manager t) }

Plus tard, vous voudrez peut-être également jeter un coup d'œil à certaines bibliothèques de programmation génériques: lorsque la complexité de vos données augmente et que vous vous retrouvez à écrire plus de code standard (comme augmenter le contenu de steak pour les joueurs, les régimes des entraîneurs et le contenu de bière de observateurs) qui est encore passe-partout, même sous une forme moins verbeuse. SYB est probablement la bibliothèque la plus connue (et est fournie avec Haskell Platform). En fait, le article original sur SYB utilise un problème très similaire à démontrer l'approche:

Tenez compte des types de données suivants qui décrivent la structure organisationnelle d'une entreprise. Une entreprise est divisée en départements. Chaque département a un responsable et se compose d'un ensemble de sous-unités, où une unité est soit un seul employé, soit un département. Les managers et les employés ordinaires ne sont que des personnes qui reçoivent un salaire.

[ignoré]

Supposons maintenant que nous souhaitons augmenter le salaire de chacun dans l'entreprise d'un pourcentage spécifié. Autrement dit, nous devons écrire la fonction:

augmenter :: Float - > Entreprise -> Entreprise

(le reste est dans le papier - la lecture est recommandée)

Bien sûr, dans votre exemple, vous avez juste besoin d'accéder / modifier un morceau d'une petite structure de données afin qu'il ne nécessite pas d'approche générique (toujours la solution basée sur SYB pour votre tâche est ci-dessous) mais une fois que vous voyez le code / modèle répétitif de l'accès / modification, vous voulez vérifier ceci ou d'autres bibliothèques de programmation génériques.

{-# LANGUAGE DeriveDataTypeable #-}

import Data.Generics

data BBTeam = BBTeam { teamname :: String, 
manager :: Coach,
players :: [BBPlayer]}  deriving (Show, Data, Typeable)

data Coach = Coach { coachname :: String, 
favcussword :: String,
 diet :: Diet }  deriving (Show, Data, Typeable)

data Diet = Diet { dietname :: String, 
steaks :: Integer, 
eggs :: Integer}  deriving (Show, Data, Typeable)

data BBPlayer = BBPlayer { playername :: String, 
hits :: Integer,
era :: Double }  deriving (Show, Data, Typeable)


incS d@(Diet _ s _) = d { steaks = s+1 }

addManagerSteak :: BBTeam -> BBTeam
addManagerSteak = everywhere (mkT incS)
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top