Esiste un idioma Haskell per l’aggiornamento di una struttura dati nidificata?

Diciamo che ho il seguente modello di dati, per tenere traccia delle statistiche dei giocatori, delle squadre e degli allenatori di baseball:

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) 

Ora diciamo che i manager, che sono solitamente dei fanatici delle bistecche, vogliono mangiare ancora più bistecche – quindi dobbiamo essere in grado di aumentare il contenuto di bistecca della dieta di un manager. Ecco due possibili implementazioni per questa funzione:

1) Questo usa un sacco di pattern matching e devo ottenere tutti gli argomenti ordinati per tutti i costruttori giusto … due volte. Sembra che non si riduca molto bene o sia molto mantenibile / leggibile.

 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) Questo usa tutti i metodi di accesso forniti dalla syntax dei record di Haskell, ma è anche brutto e ripetitivo, e difficile da mantenere e leggere, penso.

 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 

La mia domanda è, uno di questi è migliore dell’altro o più preferito all’interno della comunità Haskell? C’è un modo migliore per farlo (modificare un valore in profondità all’interno di una struttura dati mantenendo il contesto)? Non sono preoccupato per l’efficienza, solo codice eleganza / generalità / manutenibilità.

Ho notato che c’è qualcosa per questo problema (o un problema simile?) In Clojure: update-in – quindi penso che sto cercando di capire l’ update-in nel contesto della programmazione funzionale e di Haskell e della tipizzazione statica.

La syntax di aggiornamento del record viene fornita di serie con il compilatore:

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

Terribile! Ma c’è un modo migliore. Ci sono diversi pacchetti su Hackage che implementano riferimenti e obiettivi funzionali, che è sicuramente quello che vuoi fare. Ad esempio, con il pacchetto fclabels , metterei caratteri di sottolineatura davanti a tutti i nomi dei tuoi record, quindi scrivi

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

Modificato nel 2017 per aggiungere: in questi giorni vi è ampio consenso sul fatto che il pacchetto di lenti sia una tecnica di implementazione particolarmente valida. Sebbene sia un pacchetto molto grande, c’è anche un’ottima documentazione e materiale introduttivo disponibile in vari luoghi del web.

Ecco come potresti usare i combinatori di editor di semantica (SEC), come suggerito da Lambdageek.

Prima un paio di utili abbreviazioni:

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

The Unop qui è un “editor semantico”, e Lifter è il combinatore di editor semantico. Alcuni sollevatori:

 onManager :: Lifter Coach BBTeam onManager f (BBTeam nmp) = BBTeam n (fm) p onDiet :: Lifter Diet Coach onDiet f (Coach ncd) = Coach nc (fd) onStakes :: Lifter Integer Diet onStakes f (Diet nse) = Diet n (fs) e 

Ora semplicemente componi i SEC per dire quello che vuoi, cioè aggiungi 1 alle puntate della dieta del manager (di una squadra):

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

Confrontando con l’approccio SYB, la versione SEC richiede un lavoro extra per definire i SEC, e ho fornito solo quelli necessari in questo esempio. La SEC consente un’applicazione mirata, che sarebbe utile se i giocatori avessero una dieta ma non volevamo modificarli. Forse c’è anche un bel modo SYB per gestire questa distinzione.

Modifica: ecco uno stile alternativo per i SEC di base:

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

In seguito potresti anche dare un’occhiata ad alcune librerie di programmazione generiche: quando la complessità dei tuoi dati aumenta e ti ritrovi a scrivere più codice e codice (come l’aumento del contenuto di bistecca per i giocatori, le diete degli allenatori e il contenuto di birra degli osservatori) che è ancora boilerplate anche in forma meno dettagliata. SYB è probabilmente la libreria più conosciuta (e viene fornita con Haskell Platform). In effetti il documento originale su SYB utilizza un problema molto simile per dimostrare l’approccio:

Considera i seguenti tipi di dati che descrivono la struttura organizzativa di un’azienda. Una società è divisa in reparti. Ogni dipartimento ha un manager e consiste in una raccolta di sottounità, in cui un’unità è un singolo dipendente o un dipartimento. Sia i dirigenti che i dipendenti ordinari sono solo persone che ricevono un salario.

[Skiped]

Supponiamo ora di voler aumentare lo stipendio di tutti in azienda di una percentuale specifica. Cioè, dobbiamo scrivere la funzione:

aumento :: Float -> Azienda -> Azienda

(il resto è nella carta – la lettura è raccomandata)

Ovviamente nel tuo esempio hai solo bisogno di accedere / modificare un pezzo di una piccola struttura dati in modo da non richiedere un approccio generico (ancora la soluzione basata su SYB per il tuo compito è sotto) ma una volta che vedi ripetere codice / pattern di accesso / modifica tu vuoi controllare questa o altre librerie di programmazione generiche.

 {-# 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)